diff --git a/common/calculate-metrics.ts b/common/calculate-metrics.ts index 3aad1a9c..b27ac977 100644 --- a/common/calculate-metrics.ts +++ b/common/calculate-metrics.ts @@ -116,12 +116,12 @@ const calculateProfitForPeriod = ( return currentProfit } - const startingProfit = calculateTotalProfit(startingPortfolio) + const startingProfit = calculatePortfolioProfit(startingPortfolio) return currentProfit - startingProfit } -const calculateTotalProfit = (portfolio: PortfolioMetrics) => { +export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => { return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits } @@ -129,7 +129,7 @@ export const calculateNewProfit = ( portfolioHistory: PortfolioMetrics[], newPortfolio: PortfolioMetrics ) => { - const allTimeProfit = calculateTotalProfit(newPortfolio) + const allTimeProfit = calculatePortfolioProfit(newPortfolio) const descendingPortfolio = sortBy( portfolioHistory, (p) => p.timestamp diff --git a/common/comment.ts b/common/comment.ts index 3a4bd9ac..7ecbb6d4 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,6 +1,6 @@ import type { JSONContent } from '@tiptap/core' -export type AnyCommentType = OnContract | OnGroup +export type AnyCommentType = OnContract | OnGroup | OnPost // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. @@ -20,7 +20,7 @@ export type Comment = { userAvatarUrl?: string } & T -type OnContract = { +export type OnContract = { commentType: 'contract' contractId: string answerOutcome?: string @@ -35,10 +35,16 @@ type OnContract = { betOutcome?: string } -type OnGroup = { +export type OnGroup = { commentType: 'group' groupId: string } +export type OnPost = { + commentType: 'post' + postId: string +} + export type ContractComment = Comment export type GroupComment = Comment +export type PostComment = Comment diff --git a/common/envs/prod.ts b/common/envs/prod.ts index 2b1ee70e..b3b552eb 100644 --- a/common/envs/prod.ts +++ b/common/envs/prod.ts @@ -73,6 +73,7 @@ export const PROD_CONFIG: EnvConfig = { 'manticmarkets@gmail.com', // Manifold 'iansphilips@gmail.com', // Ian 'd4vidchee@gmail.com', // D4vid + 'federicoruizcassarino@gmail.com', // Fede ], visibility: 'PUBLIC', diff --git a/common/payouts-dpm.ts b/common/payouts-dpm.ts index 7d4a0185..bf6f5ebc 100644 --- a/common/payouts-dpm.ts +++ b/common/payouts-dpm.ts @@ -13,7 +13,6 @@ import { addObjects } from './util/object' export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => { const { pool } = contract const poolTotal = sum(Object.values(pool)) - console.log('resolved N/A, pool M$', poolTotal) const betSum = sumBy(bets, (b) => b.amount) @@ -58,17 +57,6 @@ export const getDpmStandardPayouts = ( liquidityFee: 0, }) - console.log( - 'resolved', - outcome, - 'pool', - poolTotal, - 'profits', - profits, - 'creator fee', - creatorFee - ) - return { payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), creatorPayout: creatorFee, @@ -110,17 +98,6 @@ export const getNumericDpmPayouts = ( liquidityFee: 0, }) - console.log( - 'resolved numeric bucket: ', - outcome, - 'pool', - poolTotal, - 'profits', - profits, - 'creator fee', - creatorFee - ) - return { payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), creatorPayout: creatorFee, @@ -163,17 +140,6 @@ export const getDpmMktPayouts = ( liquidityFee: 0, }) - console.log( - 'resolved MKT', - p, - 'pool', - pool, - 'profits', - profits, - 'creator fee', - creatorFee - ) - return { payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), creatorPayout: creatorFee, @@ -216,16 +182,6 @@ export const getPayoutsMultiOutcome = ( liquidityFee: 0, }) - console.log( - 'resolved', - resolutions, - 'pool', - poolTotal, - 'profits', - profits, - 'creator fee', - creatorFee - ) return { payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), creatorPayout: creatorFee, diff --git a/common/payouts-fixed.ts b/common/payouts-fixed.ts index 4b8de85a..99e03fac 100644 --- a/common/payouts-fixed.ts +++ b/common/payouts-fixed.ts @@ -1,4 +1,3 @@ -import { sum } from 'lodash' import { Bet } from './bet' import { getProbability } from './calculate' @@ -43,18 +42,6 @@ export const getStandardFixedPayouts = ( const { collectedFees } = contract const creatorPayout = collectedFees.creatorFee - - console.log( - 'resolved', - outcome, - 'pool', - contract.pool[outcome], - 'payouts', - sum(payouts), - 'creator fee', - creatorPayout - ) - const liquidityPayouts = getLiquidityPoolPayouts( contract, outcome, @@ -98,18 +85,6 @@ export const getMktFixedPayouts = ( const { collectedFees } = contract const creatorPayout = collectedFees.creatorFee - - console.log( - 'resolved PROB', - p, - 'pool', - p * contract.pool.YES + (1 - p) * contract.pool.NO, - 'payouts', - sum(payouts), - 'creator fee', - creatorPayout - ) - const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities) return { payouts, creatorPayout, liquidityPayouts, collectedFees } diff --git a/common/redeem.ts b/common/redeem.ts index e0839ff8..f786a1c2 100644 --- a/common/redeem.ts +++ b/common/redeem.ts @@ -13,7 +13,10 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => { const yesShares = sumBy(yesBets, (b) => b.shares) const noShares = sumBy(noBets, (b) => b.shares) const shares = Math.max(Math.min(yesShares, noShares), 0) - const soldFrac = shares > 0 ? Math.min(yesShares, noShares) / shares : 0 + const soldFrac = + shares > 0 + ? Math.min(yesShares, noShares) / Math.max(yesShares, noShares) + : 0 const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) const loanPayment = loanAmount * soldFrac const netAmount = shares - loanPayment diff --git a/common/scoring.ts b/common/scoring.ts index 39a342fd..4ef46534 100644 --- a/common/scoring.ts +++ b/common/scoring.ts @@ -1,8 +1,8 @@ -import { groupBy, sumBy, mapValues, partition } from 'lodash' +import { groupBy, sumBy, mapValues } from 'lodash' import { Bet } from './bet' +import { getContractBetMetrics } from './calculate' import { Contract } from './contract' -import { getPayouts } from './payouts' export function scoreCreators(contracts: Contract[]) { const creatorScore = mapValues( @@ -30,46 +30,8 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) { } export function scoreUsersByContract(contract: Contract, bets: Bet[]) { - const { resolution } = contract - const resolutionProb = - contract.outcomeType == 'BINARY' - ? contract.resolutionProbability - : undefined - - const [closedBets, openBets] = partition( - bets, - (bet) => bet.isSold || bet.sale - ) - const { payouts: resolvePayouts } = getPayouts( - resolution as string, - contract, - openBets, - [], - {}, - resolutionProb - ) - - const salePayouts = closedBets.map((bet) => { - const { userId, sale } = bet - return { userId, payout: sale ? sale.amount : 0 } - }) - - const investments = bets - .filter((bet) => !bet.sale) - .map((bet) => { - const { userId, amount, loanAmount } = bet - const payout = -amount - (loanAmount ?? 0) - return { userId, payout } - }) - - const netPayouts = [...resolvePayouts, ...salePayouts, ...investments] - - const userScore = mapValues( - groupBy(netPayouts, (payout) => payout.userId), - (payouts) => sumBy(payouts, ({ payout }) => payout) - ) - - return userScore + const betsByUser = groupBy(bets, bet => bet.userId) + return mapValues(betsByUser, bets => getContractBetMetrics(contract, bets).profit) } export function addUserScores( diff --git a/common/user.ts b/common/user.ts index 0e333278..f15865cf 100644 --- a/common/user.ts +++ b/common/user.ts @@ -34,7 +34,7 @@ export type User = { followerCountCached: number followedCategories?: string[] - homeSections?: { visible: string[]; hidden: string[] } + homeSections?: string[] referredByUserId?: string referredByContractId?: string diff --git a/docs/docs/api.md b/docs/docs/api.md index e284abdf..64e26de8 100644 --- a/docs/docs/api.md +++ b/docs/docs/api.md @@ -60,23 +60,27 @@ Parameters: Requires no authorization. -### `GET /v0/groups/[slug]` +### `GET /v0/group/[slug]` Gets a group by its slug. -Requires no authorization. +Requires no authorization. +Note: group is singular in the URL. ### `GET /v0/group/by-id/[id]` Gets a group by its unique ID. -Requires no authorization. +Requires no authorization. +Note: group is singular in the URL. ### `GET /v0/group/by-id/[id]/markets` Gets a group's markets by its unique ID. -Requires no authorization. +Requires no authorization. +Note: group is singular in the URL. + ### `GET /v0/markets` diff --git a/firestore.rules b/firestore.rules index 15b60d0f..9a72e454 100644 --- a/firestore.rules +++ b/firestore.rules @@ -12,7 +12,9 @@ service cloud.firestore { 'taowell@gmail.com', 'abc.sinclair@gmail.com', 'manticmarkets@gmail.com', - 'iansphilips@gmail.com' + 'iansphilips@gmail.com', + 'd4vidchee@gmail.com', + 'federicoruizcassarino@gmail.com' ] } @@ -203,6 +205,10 @@ service cloud.firestore { .affectedKeys() .hasOnly(['name', 'content']); allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId; + match /comments/{commentId} { + allow read; + allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ; + } } } } diff --git a/functions/src/change-user-info.ts b/functions/src/change-user-info.ts index aa041856..ca66f1ba 100644 --- a/functions/src/change-user-info.ts +++ b/functions/src/change-user-info.ts @@ -37,6 +37,45 @@ export const changeUser = async ( avatarUrl?: string } ) => { + // Update contracts, comments, and answers outside of a transaction to avoid contention. + // Using bulkWriter to supports >500 writes at a time + const contractsRef = firestore + .collection('contracts') + .where('creatorId', '==', user.id) + + const contracts = await contractsRef.get() + + const contractUpdate: Partial = removeUndefinedProps({ + creatorName: update.name, + creatorUsername: update.username, + creatorAvatarUrl: update.avatarUrl, + }) + + const commentSnap = await firestore + .collectionGroup('comments') + .where('userUsername', '==', user.username) + .get() + + const commentUpdate: Partial = removeUndefinedProps({ + userName: update.name, + userUsername: update.username, + userAvatarUrl: update.avatarUrl, + }) + + const answerSnap = await firestore + .collectionGroup('answers') + .where('username', '==', user.username) + .get() + const answerUpdate: Partial = removeUndefinedProps(update) + + const bulkWriter = firestore.bulkWriter() + commentSnap.docs.forEach((d) => bulkWriter.update(d.ref, commentUpdate)) + answerSnap.docs.forEach((d) => bulkWriter.update(d.ref, answerUpdate)) + contracts.docs.forEach((d) => bulkWriter.update(d.ref, contractUpdate)) + await bulkWriter.flush() + console.log('Done writing!') + + // Update the username inside a transaction return await firestore.runTransaction(async (transaction) => { if (update.username) { update.username = cleanUsername(update.username) @@ -58,42 +97,7 @@ export const changeUser = async ( const userRef = firestore.collection('users').doc(user.id) const userUpdate: Partial = removeUndefinedProps(update) - - const contractsRef = firestore - .collection('contracts') - .where('creatorId', '==', user.id) - - const contracts = await transaction.get(contractsRef) - - const contractUpdate: Partial = removeUndefinedProps({ - creatorName: update.name, - creatorUsername: update.username, - creatorAvatarUrl: update.avatarUrl, - }) - - const commentSnap = await transaction.get( - firestore - .collectionGroup('comments') - .where('userUsername', '==', user.username) - ) - - const commentUpdate: Partial = removeUndefinedProps({ - userName: update.name, - userUsername: update.username, - userAvatarUrl: update.avatarUrl, - }) - - const answerSnap = await transaction.get( - firestore - .collectionGroup('answers') - .where('username', '==', user.username) - ) - const answerUpdate: Partial = removeUndefinedProps(update) - transaction.update(userRef, userUpdate) - commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate)) - answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate)) - contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate)) }) } diff --git a/functions/src/email-templates/creating-market.html b/functions/src/email-templates/creating-market.html index a61e8d65..df215bdc 100644 --- a/functions/src/email-templates/creating-market.html +++ b/functions/src/email-templates/creating-market.html @@ -186,8 +186,9 @@ font-family: Readex Pro, Arial, Helvetica, sans-serif; font-size: 17px; - ">Did you know you create your own prediction market on Manifold for + ">Did you know you can create your own prediction market on Manifold on any question you care about?

diff --git a/functions/src/place-bet.ts b/functions/src/place-bet.ts index 404fda50..d98430c1 100644 --- a/functions/src/place-bet.ts +++ b/functions/src/place-bet.ts @@ -135,7 +135,7 @@ export const placebet = newEndpoint({}, async (req, auth) => { !isFinite(newP) || Math.min(...Object.values(newPool ?? {})) < CPMM_MIN_POOL_QTY) ) { - throw new APIError(400, 'Bet too large for current liquidity pool.') + throw new APIError(400, 'Trade too large for current liquidity pool.') } const betDoc = contractDoc.collection('bets').doc() diff --git a/web/components/amount-input.tsx b/web/components/amount-input.tsx index 9eff26ef..2ad745a8 100644 --- a/web/components/amount-input.tsx +++ b/web/components/amount-input.tsx @@ -122,6 +122,18 @@ export function BuyAmountInput(props: { } } + const parseRaw = (x: number) => { + if (x <= 100) return x + if (x <= 130) return 100 + (x - 100) * 5 + return 250 + (x - 130) * 10 + } + + const getRaw = (x: number) => { + if (x <= 100) return x + if (x <= 250) return 100 + (x - 100) / 5 + return 130 + (x - 250) / 10 + } + return ( <> onAmountChange(parseInt(e.target.value))} - className="range range-lg z-40 mb-2 xl:hidden" + max="205" + value={getRaw(amount ?? 0)} + onChange={(e) => onAmountChange(parseRaw(parseInt(e.target.value)))} + className="range range-lg only-thumb z-40 mb-2 xl:hidden" step="5" /> )} diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index ace06b6c..3339ded5 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import React, { useEffect, useRef, useState } from 'react' +import React, { useState } from 'react' import { XIcon } from '@heroicons/react/solid' import { Answer } from 'common/answer' @@ -25,8 +25,7 @@ import { import { Bet } from 'common/bet' import { track } from 'web/lib/service/analytics' import { BetSignUpPrompt } from '../sign-up-prompt' -import { isIOS } from 'web/lib/util/device' -import { AlertBox } from '../alert-box' +import { WarningConfirmationButton } from '../warning-confirmation-button' export function AnswerBetPanel(props: { answer: Answer @@ -44,12 +43,6 @@ export function AnswerBetPanel(props: { const [error, setError] = useState() const [isSubmitting, setIsSubmitting] = useState(false) - const inputRef = useRef(null) - useEffect(() => { - if (isIOS()) window.scrollTo(0, window.scrollY + 200) - inputRef.current && inputRef.current.focus() - }, []) - async function submitBet() { if (!user || !betAmount) return @@ -116,11 +109,20 @@ export function AnswerBetPanel(props: { const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) + const warning = + (betAmount ?? 0) >= 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1 + ? `You might not want to spend ${formatPercent( + bankrollFraction + )} of your balance on a single bet. \n\nCurrent balance: ${formatMoney( + user?.balance ?? 0 + )}` + : undefined + return (
- Bet on {isModal ? `"${answer.text}"` : 'this answer'} + Buy answer: {isModal ? `"${answer.text}"` : 'this answer'}
{!isModal && ( @@ -144,25 +146,9 @@ export function AnswerBetPanel(props: { error={error} setError={setError} disabled={isSubmitting} - inputRef={inputRef} showSliderOnMobile /> - {(betAmount ?? 0) > 10 && - bankrollFraction >= 0.5 && - bankrollFraction <= 1 ? ( - - ) : ( - '' - )} -
Probability
@@ -198,16 +184,17 @@ export function AnswerBetPanel(props: { {user ? ( - + /> ) : ( )} diff --git a/web/components/answers/answers-panel.tsx b/web/components/answers/answers-panel.tsx index e53153b1..5811403f 100644 --- a/web/components/answers/answers-panel.tsx +++ b/web/components/answers/answers-panel.tsx @@ -194,7 +194,7 @@ function OpenAnswer(props: { return ( - + void + user: User | null | undefined + homeSections: string[] + setHomeSections: (sections: string[]) => void }) { const { user, homeSections, setHomeSections } = props const groups = useMemberGroups(user?.id) ?? [] - const { itemsById, visibleItems, hiddenItems } = getHomeItems( - groups, - homeSections - ) + const { itemsById, sections } = getHomeItems(groups, homeSections) return ( { - console.log('drag end', e) const { destination, source, draggableId } = e if (!destination) return const item = itemsById[draggableId] - const newHomeSections = { - visible: visibleItems.map((item) => item.id), - hidden: hiddenItems.map((item) => item.id), - } + const newHomeSections = sections.map((section) => section.id) - const sourceSection = source.droppableId as 'visible' | 'hidden' - newHomeSections[sourceSection].splice(source.index, 1) - - const destSection = destination.droppableId as 'visible' | 'hidden' - newHomeSections[destSection].splice(destination.index, 0, item.id) + newHomeSections.splice(source.index, 1) + newHomeSections.splice(destination.index, 0, item.id) setHomeSections(newHomeSections) }} > - - - + + ) @@ -65,16 +51,13 @@ function DraggableList(props: { const { title, items } = props return ( - {(provided, snapshot) => ( + {(provided) => ( - + {items.map((item, index) => ( {(provided, snapshot) => ( @@ -83,16 +66,13 @@ function DraggableList(props: { {...provided.draggableProps} {...provided.dragHandleProps} style={provided.draggableProps.style} - className={clsx( - 'flex flex-row items-center gap-4 rounded bg-gray-50 p-2', - snapshot.isDragging && 'z-[9000] bg-gray-300' - )} > - @@ -104,15 +84,33 @@ function DraggableList(props: { ) } -export const getHomeItems = ( - groups: Group[], - homeSections: { visible: string[]; hidden: string[] } -) => { +const SectionItem = (props: { + item: { id: string; label: string } + className?: string +}) => { + const { item, className } = props + + return ( +
+
+ ) +} + +export const getHomeItems = (groups: Group[], sections: string[]) => { const items = [ + { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, - { label: 'Newest', id: 'newest' }, - { label: 'Close date', id: 'close-date' }, - { label: 'Your bets', id: 'your-bets' }, + { label: 'New for you', id: 'newest' }, ...groups.map((g) => ({ label: g.name, id: g.id, @@ -120,23 +118,13 @@ export const getHomeItems = ( ] const itemsById = keyBy(items, 'id') - const { visible, hidden } = homeSections + const sectionItems = filterDefined(sections.map((id) => itemsById[id])) - const [visibleItems, hiddenItems] = [ - filterDefined(visible.map((id) => itemsById[id])), - filterDefined(hidden.map((id) => itemsById[id])), - ] - - // Add unmentioned items to the visible list. - visibleItems.push( - ...items.filter( - (item) => !visibleItems.includes(item) && !hiddenItems.includes(item) - ) - ) + // Add unmentioned items to the end. + sectionItems.push(...items.filter((item) => !sectionItems.includes(item))) return { - visibleItems, - hiddenItems, + sections: sectionItems, itemsById, } } diff --git a/web/components/avatar.tsx b/web/components/avatar.tsx index 55cf3169..44c37128 100644 --- a/web/components/avatar.tsx +++ b/web/components/avatar.tsx @@ -2,6 +2,7 @@ import Router from 'next/router' import clsx from 'clsx' import { MouseEvent, useEffect, useState } from 'react' import { UserCircleIcon, UserIcon, UsersIcon } from '@heroicons/react/solid' +import Image from 'next/future/image' export function Avatar(props: { username?: string @@ -14,6 +15,7 @@ export function Avatar(props: { const [avatarUrl, setAvatarUrl] = useState(props.avatarUrl) useEffect(() => setAvatarUrl(props.avatarUrl), [props.avatarUrl]) const s = size == 'xs' ? 6 : size === 'sm' ? 8 : size || 10 + const sizeInPx = s * 4 const onClick = noLink && username @@ -26,7 +28,9 @@ export function Avatar(props: { // there can be no avatar URL or username in the feed, we show a "submit comment" // item with a fake grey user circle guy even if you aren't signed in return avatarUrl ? ( - setOpen(true)} > - Bet + Predict ) : ( @@ -57,7 +60,7 @@ export default function BetButton(props: { )} - + { - // if (selected) { - // if (isIOS()) window.scrollTo(0, window.scrollY + 200) - // focusAmountInput() - // } - // }, [selected, focusAmountInput]) - function onBetChoice(choice: 'YES' | 'NO') { setOutcome(choice) setWasSubmitted(false) - focusAmountInput() + + if (!isIOS() && !isAndroid()) { + focusAmountInput() + } } function onBetChange(newAmount: number | undefined) { @@ -274,25 +271,15 @@ function BuyPanel(props: { const bankrollFraction = (betAmount ?? 0) / (user?.balance ?? 1e9) const warning = - (betAmount ?? 0) > 10 && - bankrollFraction >= 0.5 && - bankrollFraction <= 1 ? ( - = 100 && bankrollFraction >= 0.5 && bankrollFraction <= 1 + ? `You might not want to spend ${formatPercent( bankrollFraction - )} of your balance on a single bet. \n\nCurrent balance: ${formatMoney( + )} of your balance on a single trade. \n\nCurrent balance: ${formatMoney( user?.balance ?? 0 - )}`} - /> - ) : (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1 ? ( - - ) : ( - <> - ) + )}` + : (betAmount ?? 0) > 10 && probChange >= 0.3 && bankrollFraction <= 1 + ? `Are you sure you want to move the market by ${displayedDifference}?` + : undefined return ( @@ -325,8 +312,6 @@ function BuyPanel(props: { showSliderOnMobile /> - {warning} -
@@ -367,23 +352,23 @@ function BuyPanel(props: { {user && ( - + /> )} - {wasSubmitted &&
Bet submitted!
} + {wasSubmitted &&
Trade submitted!
} ) } @@ -569,7 +554,7 @@ function LimitOrderPanel(props: {
- Bet {isPseudoNumeric ? : } up to + Buy {isPseudoNumeric ? : } up to
- Bet {isPseudoNumeric ? : } down to + Buy {isPseudoNumeric ? : } down to
-
Bet
+
Predict
{!hideToggle && ( - + { setIsLimitOrder(false) track('select quick order') }} + xs={true} > Quick @@ -768,6 +754,7 @@ function QuickOrLimitBet(props: { setIsLimitOrder(true) track('select limit order') }} + xs={true} > Limit diff --git a/web/components/buttons/pill-button.tsx b/web/components/buttons/pill-button.tsx index 8e47c94e..9aa6153f 100644 --- a/web/components/buttons/pill-button.tsx +++ b/web/components/buttons/pill-button.tsx @@ -5,19 +5,19 @@ export function PillButton(props: { selected: boolean onSelect: () => void color?: string - big?: boolean + xs?: boolean children: ReactNode }) { - const { children, selected, onSelect, color, big } = props + const { children, selected, onSelect, color, xs } = props return ( + )} + + {isSubmitting && ( + + )} + + + {!user && ( + + )} + + + ) +} diff --git a/web/components/confirmation-button.tsx b/web/components/confirmation-button.tsx index bc014902..8dbe90c2 100644 --- a/web/components/confirmation-button.tsx +++ b/web/components/confirmation-button.tsx @@ -47,13 +47,13 @@ export function ConfirmationButton(props: { {children}
updateOpen(false)} > {cancelBtn?.label ?? 'Cancel'}
@@ -69,7 +69,7 @@ export function ConfirmationButton(props: {
updateOpen(true)} > {openModalBtn.icon} diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 0beedc1b..e4b7f9cf 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -69,6 +69,7 @@ type AdditionalFilter = { excludeContractIds?: string[] groupSlug?: string yourBets?: boolean + followed?: boolean } export function ContractSearch(props: { @@ -88,6 +89,7 @@ export function ContractSearch(props: { useQueryUrlParam?: boolean isWholePage?: boolean noControls?: boolean + maxResults?: number renderContracts?: ( contracts: Contract[] | undefined, loadMore: () => void @@ -107,6 +109,7 @@ export function ContractSearch(props: { useQueryUrlParam, isWholePage, noControls, + maxResults, renderContracts, } = props @@ -189,7 +192,8 @@ export function ContractSearch(props: { const contracts = state.pages .flat() .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) - const renderedContracts = state.pages.length === 0 ? undefined : contracts + const renderedContracts = + state.pages.length === 0 ? undefined : contracts.slice(0, maxResults) if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { return @@ -292,6 +296,19 @@ function ContractSearchControls(props: { const pillGroups: { name: string; slug: string }[] = memberPillGroups.length > 0 ? memberPillGroups : DEFAULT_CATEGORY_GROUPS + const personalFilters = user + ? [ + // Show contracts in groups that the user is a member of. + memberGroupSlugs + .map((slug) => `groupLinks.slug:${slug}`) + // Or, show contracts created by users the user follows + .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []), + + // Subtract contracts you bet on, to show new ones. + `uniqueBettorIds:-${user.id}`, + ] + : [] + const additionalFilters = [ additionalFilter?.creatorId ? `creatorId:${additionalFilter.creatorId}` @@ -304,6 +321,7 @@ function ContractSearchControls(props: { ? // Show contracts bet on by the user `uniqueBettorIds:${user.id}` : '', + ...(additionalFilter?.followed ? personalFilters : []), ] const facetFilters = query ? additionalFilters @@ -320,17 +338,7 @@ function ContractSearchControls(props: { state.pillFilter !== 'your-bets' ? `groupLinks.slug:${state.pillFilter}` : '', - state.pillFilter === 'personal' - ? // Show contracts in groups that the user is a member of - memberGroupSlugs - .map((slug) => `groupLinks.slug:${slug}`) - // Show contracts created by users the user follows - .concat(follows?.map((followId) => `creatorId:${followId}`) ?? []) - : '', - // Subtract contracts you bet on from For you. - state.pillFilter === 'personal' && user - ? `uniqueBettorIds:-${user.id}` - : '', + ...(state.pillFilter === 'personal' ? personalFilters : []), state.pillFilter === 'your-bets' && user ? // Show contracts bet on by the user `uniqueBettorIds:${user.id}` @@ -441,7 +449,7 @@ function ContractSearchControls(props: { selected={state.pillFilter === 'your-bets'} onSelect={selectPill('your-bets')} > - Your bets + Your trades )} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 48528029..c383d349 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -294,7 +294,7 @@ export function ExtraMobileContractDetails(props: { {volumeTranslation} diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index f376a04a..ae586725 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -135,7 +135,7 @@ export function ContractInfoDialog(props: { */} - Bettors + Traders {bettorsCount} diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index ce5c7da6..1eaf7043 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -49,7 +49,7 @@ export function ContractLeaderboard(props: { return users && users.length > 0 ? ( !bet.isAnte && bet.userId === user.id) const visibleBets = bets.filter( (bet) => !bet.isAnte && !bet.isRedemption && bet.amount !== 0 ) - const visibleLps = lps.filter((l) => !l.isAnte && l.amount > 0) + const visibleLps = lps?.filter((l) => !l.isAnte && l.amount > 0) // Load comments here, so the badge count will be correct const updatedComments = useComments(contract.id) const comments = updatedComments ?? props.comments - const betActivity = ( + const betActivity = visibleLps && ( {!user ? ( diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index 3a09a167..c6356fdd 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -114,6 +114,7 @@ export function CreatorContractsList(props: { additionalFilter={{ creatorId: creator.id, }} + persistPrefix={`user-${creator.id}`} /> ) } diff --git a/web/components/contract/extra-contract-actions-row.tsx b/web/components/contract/extra-contract-actions-row.tsx index d4918783..5d5ee4d8 100644 --- a/web/components/contract/extra-contract-actions-row.tsx +++ b/web/components/contract/extra-contract-actions-row.tsx @@ -14,6 +14,7 @@ import { Col } from 'web/components/layout/col' import { withTracking } from 'web/lib/service/analytics' import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal' import { CHALLENGES_ENABLED } from 'common/challenge' +import ChallengeIcon from 'web/lib/icons/challenge-icon' export function ExtraContractActionsRow(props: { contract: Contract }) { const { contract } = props @@ -61,9 +62,7 @@ export function ExtraContractActionsRow(props: { contract: Contract }) { )} > - + Challenge - + const { positiveChanges, negativeChanges } = changes - const count = 3 + const threshold = 0.075 + const countOverThreshold = Math.max( + positiveChanges.findIndex((c) => c.probChanges.day < threshold) + 1, + negativeChanges.findIndex((c) => c.probChanges.day > -threshold) + 1 + ) + const maxRows = Math.min(positiveChanges.length, negativeChanges.length) + const rows = Math.min(3, Math.min(maxRows, countOverThreshold)) + + const filteredPositiveChanges = positiveChanges.slice(0, rows) + const filteredNegativeChanges = negativeChanges.slice(0, rows) + + if (rows === 0) return
None
return ( - - - {positiveChanges.slice(0, count).map((contract) => ( - - + + + {filteredPositiveChanges.map((contract) => ( + + - {contract.question} + {contract.question} ))} - - {negativeChanges.slice(0, count).map((contract) => ( - - + + {filteredNegativeChanges.map((contract) => ( + + - {contract.question} + {contract.question} ))} - + ) } @@ -63,9 +80,9 @@ export function ProbChange(props: { const color = change > 0 - ? 'text-green-600' + ? 'text-green-500' : change < 0 - ? 'text-red-600' + ? 'text-red-500' : 'text-gray-600' const str = diff --git a/web/components/double-carousel.tsx b/web/components/double-carousel.tsx index da01eb5a..12538cf7 100644 --- a/web/components/double-carousel.tsx +++ b/web/components/double-carousel.tsx @@ -7,7 +7,6 @@ import { Col } from 'web/components/layout/col' export function DoubleCarousel(props: { contracts: Contract[] - seeMoreUrl?: string showTime?: ShowTime loadMore?: () => void }) { @@ -19,7 +18,7 @@ export function DoubleCarousel(props: { ? range(0, Math.floor(contracts.length / 2)).map((col) => { const i = col * 2 return ( - + text.toLocaleLowerCase().startsWith(query.toLocaleLowerCase()) // copied from https://tiptap.dev/api/nodes/mention#usage -export const mentionSuggestion = (users: User[]): Suggestion => ({ - items: ({ query }) => +export const mentionSuggestion: Suggestion = { + items: async ({ query }) => orderBy( - users.filter((u) => searchInAny(query, u.username, u.name)), + (await getCachedUsers()).filter((u) => + searchInAny(query, u.username, u.name) + ), [ (u) => [u.name, u.username].some((s) => beginsWith(s, query)), 'followerCountCached', @@ -38,7 +40,7 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({ popup = tippy('body', { getReferenceClientRect: props.clientRect as any, appendTo: () => document.body, - content: component.element, + content: component?.element, showOnCreate: true, interactive: true, trigger: 'manual', @@ -46,27 +48,27 @@ export const mentionSuggestion = (users: User[]): Suggestion => ({ }) }, onUpdate(props) { - component.updateProps(props) + component?.updateProps(props) if (!props.clientRect) { return } - popup[0].setProps({ + popup?.[0].setProps({ getReferenceClientRect: props.clientRect as any, }) }, onKeyDown(props) { if (props.event.key === 'Escape') { - popup[0].hide() + popup?.[0].hide() return true } - return (component.ref as any)?.onKeyDown(props) + return (component?.ref as any)?.onKeyDown(props) }, onExit() { - popup[0].destroy() - component.destroy() + popup?.[0].destroy() + component?.destroy() }, } }, -}) +} diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index 0878e570..55b8a958 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -6,7 +6,7 @@ import { getOutcomeProbability } from 'common/calculate' import { FeedBet } from './feed-bets' import { FeedLiquidity } from './feed-liquidity' import { FeedAnswerCommentGroup } from './feed-answer-comment-group' -import { FeedCommentThread, CommentInput } from './feed-comments' +import { FeedCommentThread, ContractCommentInput } from './feed-comments' import { User } from 'common/user' import { CommentTipMap } from 'web/hooks/use-tip-txns' import { LiquidityProvision } from 'common/liquidity-provision' @@ -72,7 +72,7 @@ export function ContractCommentsActivity(props: { return ( <> -