+ onMouseOver?: (p: P | undefined) => void
Tooltip?: TooltipComponent
}) => {
const { color, data, yScale, w, h, Tooltip } = props
@@ -71,12 +72,9 @@ export const DistributionChart =
(props: {
const onMouseOver = useEvent((mouseX: number) => {
const queryX = xScale.invert(mouseX)
const item = data[xBisector.left(data, queryX) - 1]
- if (item == null) {
- // this can happen if you are on the very left or right edge of the chart,
- // so your queryX is out of bounds
- return
- }
- return { ...item, x: queryX }
+ const result = item ? { ...item, x: queryX } : undefined
+ props.onMouseOver?.(result)
+ return result
})
return (
@@ -108,6 +106,7 @@ export const MultiValueHistoryChart =
(props: {
colors: readonly string[]
xScale: ScaleTime
yScale: ScaleContinuousNumeric
+ onMouseOver?: (p: P | undefined) => void
Tooltip?: TooltipComponent
pct?: boolean
}) => {
@@ -156,12 +155,9 @@ export const MultiValueHistoryChart =
(props: {
const onMouseOver = useEvent((mouseX: number) => {
const queryX = xScale.invert(mouseX)
const item = data[xBisector.left(data, queryX) - 1]
- if (item == null) {
- // this can happen if you are on the very left or right edge of the chart,
- // so your queryX is out of bounds
- return
- }
- return { ...item, x: queryX }
+ const result = item ? { ...item, x: queryX } : undefined
+ props.onMouseOver?.(result)
+ return result
})
return (
@@ -196,10 +192,11 @@ export const SingleValueHistoryChart =
(props: {
color: string
xScale: ScaleTime
yScale: ScaleContinuousNumeric
+ onMouseOver?: (p: P | undefined) => void
Tooltip?: TooltipComponent
pct?: boolean
}) => {
- const { color, data, pct, yScale, w, h, Tooltip } = props
+ const { color, data, yScale, w, h, Tooltip, pct } = props
const [viewXScale, setViewXScale] = useState>()
const xScale = viewXScale ?? props.xScale
@@ -235,12 +232,9 @@ export const SingleValueHistoryChart = (props: {
const onMouseOver = useEvent((mouseX: number) => {
const queryX = xScale.invert(mouseX)
const item = data[xBisector.left(data, queryX) - 1]
- if (item == null) {
- // this can happen if you are on the very left or right edge of the chart,
- // so your queryX is out of bounds
- return
- }
- return { ...item, x: queryX }
+ const result = item ? { ...item, x: queryX } : undefined
+ props.onMouseOver?.(result)
+ return result
})
return (
From 715bae57e09a72286be89f94b6a35ebd3643dea2 Mon Sep 17 00:00:00 2001
From: Marshall Polaris
Date: Thu, 29 Sep 2022 21:35:20 -0700
Subject: [PATCH 004/101] Fix date memoization in charts (#972)
* Memoize on numbers, not dates
* Use numbers instead of dates to calculate visible range
---
web/components/charts/contract/binary.tsx | 16 ++++++++--------
web/components/charts/contract/choice.tsx | 8 ++++----
.../charts/contract/pseudo-numeric.tsx | 16 ++++++++--------
web/components/charts/helpers.tsx | 10 +++++-----
4 files changed, 25 insertions(+), 25 deletions(-)
diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx
index a5740a3e..a6ba9bee 100644
--- a/web/components/charts/contract/binary.tsx
+++ b/web/components/charts/contract/binary.tsx
@@ -49,24 +49,24 @@ export const BinaryContractChart = (props: {
onMouseOver?: (p: HistoryPoint | undefined) => void
}) => {
const { contract, bets, onMouseOver } = props
- const [startDate, endDate] = getDateRange(contract)
+ const [start, end] = getDateRange(contract)
const startP = getInitialProbability(contract)
const endP = getProbability(contract)
const betPoints = useMemo(() => getBetPoints(bets), [bets])
const data = useMemo(() => {
return [
- { x: startDate, y: startP },
+ { x: new Date(start), y: startP },
...betPoints,
- { x: endDate ?? new Date(Date.now() + DAY_MS), y: endP },
+ { x: new Date(end ?? Date.now() + DAY_MS), y: endP },
]
- }, [startDate, startP, endDate, endP, betPoints])
+ }, [start, startP, end, endP, betPoints])
const rightmostDate = getRightmostVisibleDate(
- endDate,
- last(betPoints)?.x,
- new Date(Date.now())
+ end,
+ last(betPoints)?.x?.getTime(),
+ Date.now()
)
- const visibleRange = [startDate, rightmostDate]
+ const visibleRange = [start, rightmostDate]
const isMobile = useIsMobile(800)
const containerRef = useRef(null)
const width = useElementWidth(containerRef) ?? 0
diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx
index 05d3255e..127e7d9c 100644
--- a/web/components/charts/contract/choice.tsx
+++ b/web/components/charts/contract/choice.tsx
@@ -136,10 +136,10 @@ export const ChoiceContractChart = (props: {
const betPoints = useMemo(() => getBetPoints(answers, bets), [answers, bets])
const data = useMemo(
() => [
- { x: start, y: answers.map((_) => 0) },
+ { x: new Date(start), y: answers.map((_) => 0) },
...betPoints,
{
- x: end ?? new Date(Date.now() + DAY_MS),
+ x: new Date(end ?? Date.now() + DAY_MS),
y: answers.map((a) => getOutcomeProbability(contract, a.id)),
},
],
@@ -147,8 +147,8 @@ export const ChoiceContractChart = (props: {
)
const rightmostDate = getRightmostVisibleDate(
end,
- last(betPoints)?.x,
- new Date(Date.now())
+ last(betPoints)?.x?.getTime(),
+ Date.now()
)
const visibleRange = [start, rightmostDate]
const isMobile = useIsMobile(800)
diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx
index 385e56dd..1232a96c 100644
--- a/web/components/charts/contract/pseudo-numeric.tsx
+++ b/web/components/charts/contract/pseudo-numeric.tsx
@@ -62,7 +62,7 @@ export const PseudoNumericContractChart = (props: {
}) => {
const { contract, bets, onMouseOver } = props
const { min, max, isLogScale } = contract
- const [startDate, endDate] = getDateRange(contract)
+ const [start, end] = getDateRange(contract)
const scaleP = useMemo(
() => getScaleP(min, max, isLogScale),
[min, max, isLogScale]
@@ -72,18 +72,18 @@ export const PseudoNumericContractChart = (props: {
const betPoints = useMemo(() => getBetPoints(bets, scaleP), [bets, scaleP])
const data = useMemo(
() => [
- { x: startDate, y: startP },
+ { x: new Date(start), y: startP },
...betPoints,
- { x: endDate ?? new Date(Date.now() + DAY_MS), y: endP },
+ { x: new Date(end ?? Date.now() + DAY_MS), y: endP },
],
- [betPoints, startDate, startP, endDate, endP]
+ [betPoints, start, startP, end, endP]
)
const rightmostDate = getRightmostVisibleDate(
- endDate,
- last(betPoints)?.x,
- new Date(Date.now())
+ end,
+ last(betPoints)?.x?.getTime(),
+ Date.now()
)
- const visibleRange = [startDate, rightmostDate]
+ const visibleRange = [start, rightmostDate]
const isMobile = useIsMobile(800)
const containerRef = useRef(null)
const width = useElementWidth(containerRef) ?? 0
diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx
index 35c8a335..ea436213 100644
--- a/web/components/charts/helpers.tsx
+++ b/web/components/charts/helpers.tsx
@@ -259,19 +259,19 @@ export const getDateRange = (contract: Contract) => {
const { createdTime, closeTime, resolutionTime } = contract
const isClosed = !!closeTime && Date.now() > closeTime
const endDate = resolutionTime ?? (isClosed ? closeTime : null)
- return [new Date(createdTime), endDate ? new Date(endDate) : null] as const
+ return [createdTime, endDate ?? null] as const
}
export const getRightmostVisibleDate = (
- contractEnd: Date | null | undefined,
- lastActivity: Date | null | undefined,
- now: Date
+ contractEnd: number | null | undefined,
+ lastActivity: number | null | undefined,
+ now: number
) => {
if (contractEnd != null) {
return contractEnd
} else if (lastActivity != null) {
// client-DB clock divergence may cause last activity to be later than now
- return new Date(Math.max(lastActivity.getTime(), now.getTime()))
+ return Math.max(lastActivity, now)
} else {
return now
}
From 13b3613460d56f9c1b086f202e485ca338c16137 Mon Sep 17 00:00:00 2001
From: James Grugett
Date: Thu, 29 Sep 2022 23:57:45 -0500
Subject: [PATCH 005/101] Show number of limit orders
---
web/components/limit-bets.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/components/limit-bets.tsx b/web/components/limit-bets.tsx
index 606bc7e0..bd9ed246 100644
--- a/web/components/limit-bets.tsx
+++ b/web/components/limit-bets.tsx
@@ -182,7 +182,7 @@ export function OrderBookButton(props: {
size="xs"
color="blue"
>
- Order book
+ {limitBets.length} Limit orders
From b83e5db56370b6992ce0d556a39354a501eda633 Mon Sep 17 00:00:00 2001
From: ingawei <46611122+ingawei@users.noreply.github.com>
Date: Fri, 30 Sep 2022 00:41:22 -0500
Subject: [PATCH 006/101] getting rid of daisy buttons (#969)
* getting rid of daisy buttons so bet button does not turn black on mobile
---
web/components/answers/answer-bet-panel.tsx | 6 +--
.../answers/answer-resolve-panel.tsx | 32 +++++++------
web/components/bet-panel.tsx | 14 ++----
web/components/bets-list.tsx | 3 +-
web/components/button.tsx | 28 +++++++-----
web/components/confirmation-button.tsx | 34 +++++++++-----
web/components/groups/create-group-button.tsx | 7 +--
web/components/numeric-resolution-panel.tsx | 4 +-
web/components/resolution-panel.tsx | 32 +++++--------
.../warning-confirmation-button.tsx | 45 ++++++++-----------
web/components/yes-no-selector.tsx | 4 +-
web/pages/profile.tsx | 5 ++-
12 files changed, 107 insertions(+), 107 deletions(-)
diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx
index 85f61034..9867abab 100644
--- a/web/components/answers/answer-bet-panel.tsx
+++ b/web/components/answers/answer-bet-panel.tsx
@@ -184,16 +184,14 @@ export function AnswerBetPanel(props: {
{user ? (
) : (
diff --git a/web/components/answers/answer-resolve-panel.tsx b/web/components/answers/answer-resolve-panel.tsx
index ddb7942c..57871cb8 100644
--- a/web/components/answers/answer-resolve-panel.tsx
+++ b/web/components/answers/answer-resolve-panel.tsx
@@ -85,17 +85,6 @@ export function AnswerResolvePanel(props: {
setIsSubmitting(false)
}
- const resolutionButtonClass =
- resolveOption === 'CANCEL'
- ? 'bg-yellow-400 hover:bg-yellow-500'
- : resolveOption === 'CHOOSE' && answers.length
- ? 'btn-primary'
- : resolveOption === 'CHOOSE_MULTIPLE' &&
- answers.length > 1 &&
- answers.every((answer) => chosenAnswers[answer] > 0)
- ? 'bg-blue-400 hover:bg-blue-500'
- : 'btn-disabled'
-
return (
@@ -129,11 +118,28 @@ export function AnswerResolvePanel(props: {
Clear
)}
+
1 &&
+ answers.every((answer) => chosenAnswers[answer] > 0)
+ ? 'blue'
+ : 'indigo'
+ }
+ disabled={
+ !resolveOption ||
+ (resolveOption === 'CHOOSE' && !answers.length) ||
+ (resolveOption === 'CHOOSE_MULTIPLE' &&
+ (!(answers.length > 1) ||
+ !answers.every((answer) => chosenAnswers[answer] > 0)))
+ }
onResolve={onResolve}
isSubmitting={isSubmitting}
- openModalButtonClass={resolutionButtonClass}
- submitButtonClass={resolutionButtonClass}
/>
diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx
index 5d908937..e93c0e62 100644
--- a/web/components/bet-panel.tsx
+++ b/web/components/bet-panel.tsx
@@ -395,22 +395,16 @@ export function BuyPanel(props: {
)}
setSeeLimit(true)}
>
Advanced
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index 5a95f22f..0ba90c54 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -2,7 +2,6 @@ import Link from 'next/link'
import { keyBy, groupBy, mapValues, sortBy, partition, sumBy } from 'lodash'
import dayjs from 'dayjs'
import { useMemo, useState } from 'react'
-import clsx from 'clsx'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
import { Bet } from 'web/lib/firebase/bets'
@@ -599,8 +598,8 @@ function SellButton(props: {
return (
{
diff --git a/web/components/button.tsx b/web/components/button.tsx
index ea9a3e88..51e25ea1 100644
--- a/web/components/button.tsx
+++ b/web/components/button.tsx
@@ -46,21 +46,27 @@ export function Button(props: {
- updateOpen(true)}
+ disabled={openModalBtn.disabled}
+ color={openModalBtn.color}
+ size={openModalBtn.size}
>
{openModalBtn.icon}
{openModalBtn.label}
-
+
>
)
}
@@ -84,18 +91,25 @@ export function ResolveConfirmationButton(props: {
isSubmitting: boolean
openModalButtonClass?: string
submitButtonClass?: string
+ color?: ColorType
+ disabled?: boolean
}) {
- const { onResolve, isSubmitting, openModalButtonClass, submitButtonClass } =
- props
+ const {
+ onResolve,
+ isSubmitting,
+ openModalButtonClass,
+ submitButtonClass,
+ color,
+ disabled,
+ } = props
return (
)
diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx
index 7ef6e4f3..b3237eb4 100644
--- a/web/components/resolution-panel.tsx
+++ b/web/components/resolution-panel.tsx
@@ -57,17 +57,6 @@ export function ResolutionPanel(props: {
setIsSubmitting(false)
}
- const submitButtonClass =
- outcome === 'YES'
- ? 'btn-primary'
- : outcome === 'NO'
- ? 'bg-red-400 hover:bg-red-500'
- : outcome === 'CANCEL'
- ? 'bg-yellow-400 hover:bg-yellow-500'
- : outcome === 'MKT'
- ? 'bg-blue-400 hover:bg-blue-500'
- : 'btn-disabled'
-
return (
{isAdmin && !isCreator && (
@@ -76,18 +65,14 @@ export function ResolutionPanel(props: {
)}
Resolve market
-
Outcome
-
-
-
{outcome === 'YES' ? (
<>
@@ -123,16 +108,23 @@ export function ResolutionPanel(props: {
<>Resolving this market will immediately pay out {BETTORS}.>
)}
-
-
{!!error && {error}
}
-
)
diff --git a/web/components/warning-confirmation-button.tsx b/web/components/warning-confirmation-button.tsx
index 7c546c3b..abdf443e 100644
--- a/web/components/warning-confirmation-button.tsx
+++ b/web/components/warning-confirmation-button.tsx
@@ -5,17 +5,18 @@ import { Row } from './layout/row'
import { ConfirmationButton } from './confirmation-button'
import { ExclamationIcon } from '@heroicons/react/solid'
import { formatMoney } from 'common/util/format'
+import { Button, ColorType, SizeType } from './button'
export function WarningConfirmationButton(props: {
amount: number | undefined
- outcome?: 'YES' | 'NO' | undefined
marketType: 'freeResponse' | 'binary'
warning?: string
onSubmit: () => void
- disabled?: boolean
+ disabled: boolean
isSubmitting: boolean
openModalButtonClass?: string
- submitButtonClassName?: string
+ color: ColorType
+ size: SizeType
}) {
const {
amount,
@@ -24,53 +25,43 @@ export function WarningConfirmationButton(props: {
disabled,
isSubmitting,
openModalButtonClass,
- submitButtonClassName,
- outcome,
- marketType,
+ size,
+ color,
} = props
+
if (!warning) {
return (
-
{isSubmitting
? 'Submitting...'
: amount
? `Wager ${formatMoney(amount)}`
: 'Wager'}
-
+
)
}
return (
diff --git a/web/components/yes-no-selector.tsx b/web/components/yes-no-selector.tsx
index f73cdef2..10a58a42 100644
--- a/web/components/yes-no-selector.tsx
+++ b/web/components/yes-no-selector.tsx
@@ -213,7 +213,7 @@ export function NumberCancelSelector(props: {
return (
onSelect('NUMBER')}
className={clsx('whitespace-nowrap', btnClassName)}
>
@@ -244,7 +244,7 @@ function Button(props: {
type="button"
className={clsx(
'inline-flex flex-1 items-center justify-center rounded-md border border-transparent px-8 py-3 font-medium shadow-sm',
- color === 'green' && 'btn-primary text-white',
+ color === 'green' && 'bg-teal-500 bg-teal-600 text-white',
color === 'red' && 'bg-red-400 text-white hover:bg-red-500',
color === 'yellow' && 'bg-yellow-400 text-white hover:bg-yellow-500',
color === 'blue' && 'bg-blue-400 text-white hover:bg-blue-500',
diff --git a/web/pages/profile.tsx b/web/pages/profile.tsx
index caa9f47a..5773f30f 100644
--- a/web/pages/profile.tsx
+++ b/web/pages/profile.tsx
@@ -218,9 +218,10 @@ export default function ProfilePage(props: {
/>
,
+ icon: ,
+ color: 'indigo',
}}
submitBtn={{
label: 'Update key',
From 523689b52520daec75ec44ccdc0bac1fd3e52c02 Mon Sep 17 00:00:00 2001
From: Marshall Polaris
Date: Thu, 29 Sep 2022 22:45:31 -0700
Subject: [PATCH 007/101] Keep tooltip within bounds of chart (well, for non-FR
charts) (#970)
---
web/components/charts/helpers.tsx | 53 ++++++++++++++++++++++++-------
1 file changed, 42 insertions(+), 11 deletions(-)
diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx
index ea436213..b7948298 100644
--- a/web/components/charts/helpers.tsx
+++ b/web/components/charts/helpers.tsx
@@ -25,6 +25,8 @@ export type YScale = P extends Point ? AxisScale : never
export const MARGIN = { top: 20, right: 10, bottom: 20, left: 40 }
export const MARGIN_X = MARGIN.right + MARGIN.left
export const MARGIN_Y = MARGIN.top + MARGIN.bottom
+const MARGIN_STYLE = `${MARGIN.top}px ${MARGIN.right}px ${MARGIN.bottom}px ${MARGIN.left}px`
+const MARGIN_XFORM = `translate(${MARGIN.left}, ${MARGIN.top})`
export const XAxis = (props: { w: number; h: number; axis: Axis }) => {
const { h, axis } = props
@@ -128,7 +130,7 @@ export const SVGChart = >(props: {
Tooltip?: TooltipComponent
}) => {
const { children, w, h, xAxis, yAxis, onMouseOver, onSelect, Tooltip } = props
- const [mouseState, setMouseState] = useState()
+ const [mouseState, setMouseState] = useState<{ pos: TooltipPosition; p: P }>()
const overlayRef = useRef(null)
const innerW = w - MARGIN_X
const innerH = h - MARGIN_Y
@@ -170,7 +172,8 @@ export const SVGChart = >(props: {
const [mouseX, mouseY] = pointer(ev)
const p = onMouseOver(mouseX, mouseY)
if (p != null) {
- setMouseState({ top: mouseY - 10, left: mouseX + 60, p })
+ const pos = getTooltipPosition(mouseX, mouseY, innerW, innerH)
+ setMouseState({ pos, p })
} else {
setMouseState(undefined)
}
@@ -184,15 +187,15 @@ export const SVGChart = >(props: {
return (
{mouseState && Tooltip && (
-
+
)}
-
+
-
+
{children}
@@ -214,20 +217,48 @@ export const SVGChart = >(props: {
)
}
+export type TooltipPosition = {
+ top?: number
+ right?: number
+ bottom?: number
+ left?: number
+}
+
+export const getTooltipPosition = (
+ mouseX: number,
+ mouseY: number,
+ w: number,
+ h: number
+) => {
+ const result: TooltipPosition = {}
+ if (mouseX <= (3 * w) / 4) {
+ result.left = mouseX + 10 // in the left three quarters
+ } else {
+ result.right = w - mouseX + 10 // in the right quarter
+ }
+ if (mouseY <= h / 4) {
+ result.top = mouseY + 10 // in the top quarter
+ } else {
+ result.bottom = h - mouseY + 10 // in the bottom three quarters
+ }
+ return result
+}
+
export type TooltipProps = { p: P; xScale: XScale
}
export type TooltipComponent
= React.ComponentType>
-export type TooltipPosition = { top: number; left: number }
-export const TooltipContainer = (
- props: TooltipPosition & { className?: string; children: React.ReactNode }
-) => {
- const { top, left, className, children } = props
+export const TooltipContainer = (props: {
+ pos: TooltipPosition
+ className?: string
+ children: React.ReactNode
+}) => {
+ const { pos, className, children } = props
return (
{children}
From 7e91133229e176527e06bed3fa3d3ceacd3e2c5d Mon Sep 17 00:00:00 2001
From: Marshall Polaris
Date: Thu, 29 Sep 2022 22:45:51 -0700
Subject: [PATCH 008/101] Change styles on contract tooltips to be more like
portfolio graph (#966)
---
web/components/charts/contract/binary.tsx | 6 ++--
web/components/charts/contract/choice.tsx | 34 ++++++++++++++++---
web/components/charts/contract/numeric.tsx | 7 ++--
.../charts/contract/pseudo-numeric.tsx | 6 ++--
web/components/charts/helpers.tsx | 24 +------------
5 files changed, 40 insertions(+), 37 deletions(-)
diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx
index a6ba9bee..264e1e3b 100644
--- a/web/components/charts/contract/binary.tsx
+++ b/web/components/charts/contract/binary.tsx
@@ -34,10 +34,10 @@ const BinaryChartTooltip = (props: TooltipProps>) => {
const { x, y, datum } = p
const [start, end] = xScale.domain()
return (
-
+
{datum && }
- {formatPct(y)}
- {formatDateInRange(x, start, end)}
+ {formatDateInRange(x, start, end)}
+ {formatPct(y)}
)
}
diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx
index 127e7d9c..1908f98f 100644
--- a/web/components/charts/contract/choice.tsx
+++ b/web/components/charts/contract/choice.tsx
@@ -9,7 +9,6 @@ import { getOutcomeProbability } from 'common/calculate'
import { useIsMobile } from 'web/hooks/use-is-mobile'
import { DAY_MS } from 'common/util/time'
import {
- Legend,
TooltipProps,
MARGIN_X,
MARGIN_Y,
@@ -121,6 +120,29 @@ const getBetPoints = (answers: Answer[], bets: Bet[]) => {
return points
}
+type LegendItem = { color: string; label: string; value?: string }
+const Legend = (props: { className?: string; items: LegendItem[] }) => {
+ const { items, className } = props
+ return (
+
+ {items.map((item) => (
+
+
+
+
+ {item.label}
+
+
+ {item.value}
+
+ ))}
+
+ )
+}
+
export const ChoiceContractChart = (props: {
contract: FreeResponseContract | MultipleChoiceContract
bets: Bet[]
@@ -173,13 +195,15 @@ export const ChoiceContractChart = (props: {
(item) => -item.p
).slice(0, 10)
return (
-
+ <>
{datum && }
- {formatDateInRange(x, start, end)}
+
+ {formatDateInRange(x, start, end)}
+
-
-
+
+ >
)
},
[answers]
diff --git a/web/components/charts/contract/numeric.tsx b/web/components/charts/contract/numeric.tsx
index dd031ab8..ac300361 100644
--- a/web/components/charts/contract/numeric.tsx
+++ b/web/components/charts/contract/numeric.tsx
@@ -24,9 +24,10 @@ const getNumericChartData = (contract: NumericContract) => {
const NumericChartTooltip = (props: TooltipProps) => {
const { x, y } = props.p
return (
-
- {formatPct(y, 2)} {formatLargeNumber(x)}
-
+ <>
+ {formatLargeNumber(x)}
+ {formatPct(y, 2)}
+ >
)
}
diff --git a/web/components/charts/contract/pseudo-numeric.tsx b/web/components/charts/contract/pseudo-numeric.tsx
index 1232a96c..adf2e493 100644
--- a/web/components/charts/contract/pseudo-numeric.tsx
+++ b/web/components/charts/contract/pseudo-numeric.tsx
@@ -46,10 +46,10 @@ const PseudoNumericChartTooltip = (props: TooltipProps>) => {
const { x, y, datum } = p
const [start, end] = xScale.domain()
return (
-
+
{datum && }
- {formatLargeNumber(y)}
- {formatDateInRange(x, start, end)}
+ {formatDateInRange(x, start, end)}
+ {formatLargeNumber(y)}
)
}
diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx
index b7948298..acd88a4f 100644
--- a/web/components/charts/helpers.tsx
+++ b/web/components/charts/helpers.tsx
@@ -16,7 +16,6 @@ import dayjs from 'dayjs'
import clsx from 'clsx'
import { Contract } from 'common/contract'
-import { Row } from 'web/components/layout/row'
export type Point = { x: X; y: Y; datum?: T }
export type XScale = P extends Point ? AxisScale : never
@@ -256,7 +255,7 @@ export const TooltipContainer = (props: {
@@ -265,27 +264,6 @@ export const TooltipContainer = (props: {
)
}
-export type LegendItem = { color: string; label: string; value?: string }
-export const Legend = (props: { className?: string; items: LegendItem[] }) => {
- const { items, className } = props
- return (
-
- {items.map((item) => (
-
-
-
- {item.label}
-
- {item.value}
-
- ))}
-
- )
-}
-
export const getDateRange = (contract: Contract) => {
const { createdTime, closeTime, resolutionTime } = contract
const isClosed = !!closeTime && Date.now() > closeTime
From f892c92e262d027f6198d75c21bc0eafad7b0085 Mon Sep 17 00:00:00 2001
From: James Grugett
Date: Fri, 30 Sep 2022 01:11:04 -0500
Subject: [PATCH 009/101] Save portfolio sort and filter to local storage!
---
web/components/bets-list.tsx | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx
index 0ba90c54..c2773741 100644
--- a/web/components/bets-list.tsx
+++ b/web/components/bets-list.tsx
@@ -45,6 +45,11 @@ import { UserLink } from 'web/components/user-link'
import { useUserBetContracts } from 'web/hooks/use-contracts'
import { BetsSummary } from './bet-summary'
import { ProfitBadge } from './profit-badge'
+import {
+ storageStore,
+ usePersistentState,
+} from 'web/hooks/use-persistent-state'
+import { safeLocalStorage } from 'web/lib/util/local'
type BetSort = 'newest' | 'profit' | 'closeTime' | 'value'
type BetFilter = 'open' | 'limit_bet' | 'sold' | 'closed' | 'resolved' | 'all'
@@ -75,8 +80,14 @@ export function BetsList(props: { user: User }) {
return contractList ? keyBy(contractList, 'id') : undefined
}, [contractList])
- const [sort, setSort] = useState('newest')
- const [filter, setFilter] = useState('all')
+ const [sort, setSort] = usePersistentState('newest', {
+ key: 'bets-list-sort',
+ store: storageStore(safeLocalStorage()),
+ })
+ const [filter, setFilter] = usePersistentState('all', {
+ key: 'bets-list-filter',
+ store: storageStore(safeLocalStorage()),
+ })
const [page, setPage] = useState(0)
const start = page * CONTRACTS_PER_PAGE
const end = start + CONTRACTS_PER_PAGE
From 95c47aba1afc1a2e4eeb22d83525eda71b53c870 Mon Sep 17 00:00:00 2001
From: mantikoros
Date: Fri, 30 Sep 2022 01:30:45 -0500
Subject: [PATCH 010/101] midterms: add CO, additional markets
---
web/pages/midterms.tsx | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/web/pages/midterms.tsx b/web/pages/midterms.tsx
index 1ae72f7a..d508743c 100644
--- a/web/pages/midterms.tsx
+++ b/web/pages/midterms.tsx
@@ -67,6 +67,12 @@ const senateMidterms: StateElectionMarket[] = [
slug: 'will-mike-lee-win-the-2022-utah-sen',
isWinRepublican: true,
},
+ {
+ state: 'CO',
+ creatorUsername: 'SG',
+ slug: 'will-michael-bennet-win-the-2022-co',
+ isWinRepublican: false,
+ },
]
const App = () => {
@@ -84,6 +90,25 @@ const App = () => {
height={400}
className="mt-8"
>
+
+ Related markets
+
+
+
)
From 608ee7b865d85d11d9e4f3e86428658a3dfc191c Mon Sep 17 00:00:00 2001
From: Marshall Polaris
Date: Fri, 30 Sep 2022 00:03:31 -0700
Subject: [PATCH 011/101] Chart visual style adjustment (#971)
* Adjust area fill opacity on line charts
* Light gray border on tooltips
---
web/components/charts/helpers.tsx | 4 ++--
web/components/portfolio/portfolio-value-graph.tsx | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/web/components/charts/helpers.tsx b/web/components/charts/helpers.tsx
index acd88a4f..ba9865b2 100644
--- a/web/components/charts/helpers.tsx
+++ b/web/components/charts/helpers.tsx
@@ -111,7 +111,7 @@ export const AreaWithTopStroke = (props: {
py1={py1}
curve={curve}
fill={color}
- opacity={0.3}
+ opacity={0.2}
/>
@@ -255,7 +255,7 @@ export const TooltipContainer = (props: {
diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx
index 6ed5d195..e329457d 100644
--- a/web/components/portfolio/portfolio-value-graph.tsx
+++ b/web/components/portfolio/portfolio-value-graph.tsx
@@ -105,7 +105,7 @@ export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
sliceTooltip={({ slice }) => {
handleGraphDisplayChange(slice.points[0].data.yFormatted)
return (
-
+
Date: Fri, 30 Sep 2022 00:05:36 -0700
Subject: [PATCH 012/101] Fix default sizes on charts to make more sense
---
web/components/charts/contract/binary.tsx | 2 +-
web/components/charts/contract/choice.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/components/charts/contract/binary.tsx b/web/components/charts/contract/binary.tsx
index 264e1e3b..a3f04a29 100644
--- a/web/components/charts/contract/binary.tsx
+++ b/web/components/charts/contract/binary.tsx
@@ -70,7 +70,7 @@ export const BinaryContractChart = (props: {
const isMobile = useIsMobile(800)
const containerRef = useRef
(null)
const width = useElementWidth(containerRef) ?? 0
- const height = props.height ?? (isMobile ? 250 : 350)
+ const height = props.height ?? (isMobile ? 150 : 250)
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
diff --git a/web/components/charts/contract/choice.tsx b/web/components/charts/contract/choice.tsx
index 1908f98f..7c9ec07a 100644
--- a/web/components/charts/contract/choice.tsx
+++ b/web/components/charts/contract/choice.tsx
@@ -176,7 +176,7 @@ export const ChoiceContractChart = (props: {
const isMobile = useIsMobile(800)
const containerRef = useRef(null)
const width = useElementWidth(containerRef) ?? 0
- const height = props.height ?? (isMobile ? 150 : 250)
+ const height = props.height ?? (isMobile ? 250 : 350)
const xScale = scaleTime(visibleRange, [0, width - MARGIN_X])
const yScale = scaleLinear([0, 1], [height - MARGIN_Y, 0])
From c16e5189f71207895164465f58263a70ead9e78f Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Fri, 30 Sep 2022 07:53:47 -0600
Subject: [PATCH 013/101] Don't send portfolio email to user less than 5 days
old
---
functions/src/weekly-portfolio-emails.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/functions/src/weekly-portfolio-emails.ts b/functions/src/weekly-portfolio-emails.ts
index 0167be35..ab46c5f1 100644
--- a/functions/src/weekly-portfolio-emails.ts
+++ b/functions/src/weekly-portfolio-emails.ts
@@ -117,7 +117,8 @@ export async function sendPortfolioUpdateEmailsToAllUsers() {
await Promise.all(
privateUsersToSendEmailsTo.map(async (privateUser) => {
const user = await getUser(privateUser.id)
- if (!user) return
+ // Don't send to a user unless they're over 5 days old
+ if (!user || user.createdTime > Date.now() - 5 * DAY_MS) return
const userBets = usersBets[privateUser.id] as Bet[]
const contractsUserBetOn = contractsUsersBetOn.filter((contract) =>
userBets.some((bet) => bet.contractId === contract.id)
From 138f34fc666a8e18a5d2222980e0139ab20684c2 Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Fri, 30 Sep 2022 08:40:46 -0600
Subject: [PATCH 014/101] Add close now button to contract edit time
---
web/components/contract/contract-details.tsx | 28 ++++++++++++++------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx
index 3525b9f9..fc4bcfcf 100644
--- a/web/components/contract/contract-details.tsx
+++ b/web/components/contract/contract-details.tsx
@@ -356,18 +356,22 @@ function EditableCloseDate(props: {
closeTime && dayJsCloseTime.format('HH:mm')
)
- const newCloseTime = closeDate
- ? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
- : undefined
-
const isSameYear = dayJsCloseTime.isSame(dayJsNow, 'year')
const isSameDay = dayJsCloseTime.isSame(dayJsNow, 'day')
- const onSave = () => {
+ let newCloseTime = closeDate
+ ? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()
+ : undefined
+ function onSave(customTime?: number) {
+ if (customTime) {
+ newCloseTime = customTime
+ setCloseDate(dayjs(newCloseTime).format('YYYY-MM-DD'))
+ setCloseHoursMinutes(dayjs(newCloseTime).format('HH:mm'))
+ }
if (!newCloseTime) return
if (newCloseTime === closeTime) setIsEditingCloseTime(false)
- else if (newCloseTime > Date.now()) {
+ else {
const content = contract.description
const formattedCloseDate = dayjs(newCloseTime).format('YYYY-MM-DD h:mm a')
@@ -416,13 +420,21 @@ function EditableCloseDate(props: {
/>
onSave()}
>
Done
+ onSave(Date.now())}
+ >
+ Close Now
+
Date: Fri, 30 Sep 2022 08:48:33 -0600
Subject: [PATCH 015/101] Remove green circle from resolution prob input
---
web/components/probability-selector.tsx | 14 ++-------
web/components/resolution-panel.tsx | 41 ++++++++++++++-----------
2 files changed, 26 insertions(+), 29 deletions(-)
diff --git a/web/components/probability-selector.tsx b/web/components/probability-selector.tsx
index 2fc03787..b13dcfd9 100644
--- a/web/components/probability-selector.tsx
+++ b/web/components/probability-selector.tsx
@@ -8,12 +8,12 @@ export function ProbabilitySelector(props: {
const { probabilityInt, setProbabilityInt, isSubmitting } = props
return (
-
-
+
+
%
- setProbabilityInt(parseInt(e.target.value))}
- />
)
}
diff --git a/web/components/resolution-panel.tsx b/web/components/resolution-panel.tsx
index b3237eb4..f39284a5 100644
--- a/web/components/resolution-panel.tsx
+++ b/web/components/resolution-panel.tsx
@@ -11,6 +11,8 @@ import { ProbabilitySelector } from './probability-selector'
import { getProbability } from 'common/calculate'
import { BinaryContract, resolution } from 'common/contract'
import { BETTOR, BETTORS, PAST_BETS } from 'common/user'
+import { Row } from 'web/components/layout/row'
+import { capitalize } from 'lodash'
export function ResolutionPanel(props: {
isAdmin: boolean
@@ -94,9 +96,10 @@ export function ResolutionPanel(props: {
withdrawn from your account
>
) : outcome === 'MKT' ? (
-
+
- {PAST_BETS} will be paid out at the probability you specify:
+ {capitalize(PAST_BETS)} will be paid out at the probability you
+ specify:
{!!error && {error}
}
-
+
+
+
)
}
From a90b7656703a7e167c6e023044fb2ab387bca612 Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Fri, 30 Sep 2022 09:27:42 -0600
Subject: [PATCH 016/101] Bounty comments (#944)
* Adding, awarding, and sorting by bounties
* Add notification for bounty award as tip
* Fix merge
* Wording
* Allow adding in batches of m250
* import
* imports
* Style tabs
* Refund unused bounties
* Show curreantly available, reset open to 0
* Refactor
* Rerun check prs
* reset yarn.lock
* Revert "reset yarn.lock"
This reverts commit 4606984276821f403efd5ef7e1eca45b19ee4081.
* undo yarn.lock changes
* Track comment bounties
---
common/comment.ts | 1 +
common/contract.ts | 1 +
common/economy.ts | 1 +
common/envs/prod.ts | 1 +
common/txn.ts | 33 ++++
functions/src/create-notification.ts | 44 +++++
functions/src/index.ts | 6 +
functions/src/on-update-contract.ts | 134 +++++++++++----
functions/src/serve.ts | 3 +
functions/src/update-comment-bounty.ts | 162 ++++++++++++++++++
web/components/award-bounty-button.tsx | 46 +++++
.../contract/add-comment-bounty.tsx | 74 ++++++++
.../contract/bountied-contract-badge.tsx | 9 +
web/components/contract/contract-details.tsx | 3 +
.../contract/contract-info-dialog.tsx | 6 +-
web/components/contract/contract-tabs.tsx | 58 ++++++-
.../liquidity-bounty-panel.tsx} | 63 ++++---
web/components/feed/feed-comments.tsx | 29 +++-
web/components/layout/tabs.tsx | 22 ++-
web/components/tipper.tsx | 4 +-
web/components/user-page.tsx | 6 +-
web/lib/firebase/api.ts | 8 +
web/lib/firebase/comments.ts | 27 ++-
web/package.json | 2 +-
24 files changed, 648 insertions(+), 95 deletions(-)
create mode 100644 functions/src/update-comment-bounty.ts
create mode 100644 web/components/award-bounty-button.tsx
create mode 100644 web/components/contract/add-comment-bounty.tsx
create mode 100644 web/components/contract/bountied-contract-badge.tsx
rename web/components/{liquidity-panel.tsx => contract/liquidity-bounty-panel.tsx} (77%)
diff --git a/common/comment.ts b/common/comment.ts
index cdb62fd3..71c04af4 100644
--- a/common/comment.ts
+++ b/common/comment.ts
@@ -18,6 +18,7 @@ export type Comment = {
userName: string
userUsername: string
userAvatarUrl?: string
+ bountiesAwarded?: number
} & T
export type OnContract = {
diff --git a/common/contract.ts b/common/contract.ts
index 248c9745..2e9d94c4 100644
--- a/common/contract.ts
+++ b/common/contract.ts
@@ -62,6 +62,7 @@ export type Contract = {
featuredOnHomeRank?: number
likedByUserIds?: string[]
likedByUserCount?: number
+ openCommentBounties?: number
} & T
export type BinaryContract = Contract & Binary
diff --git a/common/economy.ts b/common/economy.ts
index 7ec52b30..d25a0c71 100644
--- a/common/economy.ts
+++ b/common/economy.ts
@@ -15,3 +15,4 @@ export const BETTING_STREAK_BONUS_AMOUNT =
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 50
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7
export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5
+export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250
diff --git a/common/envs/prod.ts b/common/envs/prod.ts
index d0469d84..38dd4feb 100644
--- a/common/envs/prod.ts
+++ b/common/envs/prod.ts
@@ -41,6 +41,7 @@ export type Economy = {
BETTING_STREAK_BONUS_MAX?: number
BETTING_STREAK_RESET_HOUR?: number
FREE_MARKETS_PER_USER_MAX?: number
+ COMMENT_BOUNTY_AMOUNT?: number
}
type FirebaseConfig = {
diff --git a/common/txn.ts b/common/txn.ts
index 2b7a32e8..c404059d 100644
--- a/common/txn.ts
+++ b/common/txn.ts
@@ -8,6 +8,7 @@ type AnyTxnType =
| UniqueBettorBonus
| BettingStreakBonus
| CancelUniqueBettorBonus
+ | CommentBountyRefund
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn = {
@@ -31,6 +32,8 @@ export type Txn = {
| 'UNIQUE_BETTOR_BONUS'
| 'BETTING_STREAK_BONUS'
| 'CANCEL_UNIQUE_BETTOR_BONUS'
+ | 'COMMENT_BOUNTY'
+ | 'REFUND_COMMENT_BOUNTY'
// Any extra data
data?: { [key: string]: any }
@@ -98,6 +101,34 @@ type CancelUniqueBettorBonus = {
}
}
+type CommentBountyDeposit = {
+ fromType: 'USER'
+ toType: 'BANK'
+ category: 'COMMENT_BOUNTY'
+ data: {
+ contractId: string
+ }
+}
+
+type CommentBountyWithdrawal = {
+ fromType: 'BANK'
+ toType: 'USER'
+ category: 'COMMENT_BOUNTY'
+ data: {
+ contractId: string
+ commentId: string
+ }
+}
+
+type CommentBountyRefund = {
+ fromType: 'BANK'
+ toType: 'USER'
+ category: 'REFUND_COMMENT_BOUNTY'
+ data: {
+ contractId: string
+ }
+}
+
export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink
@@ -105,3 +136,5 @@ export type ReferralTxn = Txn & Referral
export type BettingStreakBonusTxn = Txn & BettingStreakBonus
export type UniqueBettorBonusTxn = Txn & UniqueBettorBonus
export type CancelUniqueBettorBonusTxn = Txn & CancelUniqueBettorBonus
+export type CommentBountyDepositTxn = Txn & CommentBountyDeposit
+export type CommentBountyWithdrawalTxn = Txn & CommentBountyWithdrawal
diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts
index 038e0142..9bd73d05 100644
--- a/functions/src/create-notification.ts
+++ b/functions/src/create-notification.ts
@@ -1046,3 +1046,47 @@ export const createContractResolvedNotifications = async (
)
)
}
+
+export const createBountyNotification = async (
+ fromUser: User,
+ toUserId: string,
+ amount: number,
+ idempotencyKey: string,
+ contract: Contract,
+ commentId?: string
+) => {
+ const privateUser = await getPrivateUser(toUserId)
+ if (!privateUser) return
+ const { sendToBrowser } = getNotificationDestinationsForUser(
+ privateUser,
+ 'tip_received'
+ )
+ if (!sendToBrowser) return
+
+ const slug = commentId
+ const notificationRef = firestore
+ .collection(`/users/${toUserId}/notifications`)
+ .doc(idempotencyKey)
+ const notification: Notification = {
+ id: idempotencyKey,
+ userId: toUserId,
+ reason: 'tip_received',
+ createdTime: Date.now(),
+ isSeen: false,
+ sourceId: commentId ? commentId : contract.id,
+ sourceType: 'tip',
+ sourceUpdateType: 'created',
+ sourceUserName: fromUser.name,
+ sourceUserUsername: fromUser.username,
+ sourceUserAvatarUrl: fromUser.avatarUrl,
+ sourceText: amount.toString(),
+ sourceContractCreatorUsername: contract.creatorUsername,
+ sourceContractTitle: contract.question,
+ sourceContractSlug: contract.slug,
+ sourceSlug: slug,
+ sourceTitle: contract.question,
+ }
+ return await notificationRef.set(removeUndefinedProps(notification))
+
+ // maybe TODO: send email notification to comment creator
+}
diff --git a/functions/src/index.ts b/functions/src/index.ts
index 9a8ec232..f5c45004 100644
--- a/functions/src/index.ts
+++ b/functions/src/index.ts
@@ -52,6 +52,7 @@ export * from './unsubscribe'
export * from './stripe'
export * from './mana-bonus-email'
export * from './close-market'
+export * from './update-comment-bounty'
import { health } from './health'
import { transact } from './transact'
@@ -65,6 +66,7 @@ import { sellshares } from './sell-shares'
import { claimmanalink } from './claim-manalink'
import { createmarket } from './create-market'
import { addliquidity } from './add-liquidity'
+import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
import { withdrawliquidity } from './withdraw-liquidity'
import { creategroup } from './create-group'
import { resolvemarket } from './resolve-market'
@@ -91,6 +93,8 @@ const sellSharesFunction = toCloudFunction(sellshares)
const claimManalinkFunction = toCloudFunction(claimmanalink)
const createMarketFunction = toCloudFunction(createmarket)
const addLiquidityFunction = toCloudFunction(addliquidity)
+const addCommentBounty = toCloudFunction(addcommentbounty)
+const awardCommentBounty = toCloudFunction(awardcommentbounty)
const withdrawLiquidityFunction = toCloudFunction(withdrawliquidity)
const createGroupFunction = toCloudFunction(creategroup)
const resolveMarketFunction = toCloudFunction(resolvemarket)
@@ -127,4 +131,6 @@ export {
acceptChallenge as acceptchallenge,
createPostFunction as createpost,
saveTwitchCredentials as savetwitchcredentials,
+ addCommentBounty as addcommentbounty,
+ awardCommentBounty as awardcommentbounty,
}
diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts
index 5e2a94c0..d667f0d2 100644
--- a/functions/src/on-update-contract.ts
+++ b/functions/src/on-update-contract.ts
@@ -1,44 +1,118 @@
import * as functions from 'firebase-functions'
-import { getUser } from './utils'
+import { getUser, getValues, log } from './utils'
import { createCommentOrAnswerOrUpdatedContractNotification } from './create-notification'
import { Contract } from '../../common/contract'
+import { Txn } from '../../common/txn'
+import { partition, sortBy } from 'lodash'
+import { runTxn, TxnData } from './transact'
+import * as admin from 'firebase-admin'
export const onUpdateContract = functions.firestore
.document('contracts/{contractId}')
.onUpdate(async (change, context) => {
const contract = change.after.data() as Contract
+ const previousContract = change.before.data() as Contract
const { eventId } = context
-
- const contractUpdater = await getUser(contract.creatorId)
- if (!contractUpdater) throw new Error('Could not find contract updater')
-
- const previousValue = change.before.data() as Contract
-
- // Resolution is handled in resolve-market.ts
- if (!previousValue.isResolved && contract.isResolved) return
+ const { openCommentBounties, closeTime, question } = contract
if (
- previousValue.closeTime !== contract.closeTime ||
- previousValue.question !== contract.question
+ !previousContract.isResolved &&
+ contract.isResolved &&
+ (openCommentBounties ?? 0) > 0
) {
- let sourceText = ''
- if (
- previousValue.closeTime !== contract.closeTime &&
- contract.closeTime
- ) {
- sourceText = contract.closeTime.toString()
- } else if (previousValue.question !== contract.question) {
- sourceText = contract.question
- }
-
- await createCommentOrAnswerOrUpdatedContractNotification(
- contract.id,
- 'contract',
- 'updated',
- contractUpdater,
- eventId,
- sourceText,
- contract
- )
+ await handleUnusedCommentBountyRefunds(contract)
+ // No need to notify users of resolution, that's handled in resolve-market
+ return
+ }
+ if (
+ previousContract.closeTime !== closeTime ||
+ previousContract.question !== question
+ ) {
+ await handleUpdatedCloseTime(previousContract, contract, eventId)
}
})
+
+async function handleUpdatedCloseTime(
+ previousContract: Contract,
+ contract: Contract,
+ eventId: string
+) {
+ const contractUpdater = await getUser(contract.creatorId)
+ if (!contractUpdater) throw new Error('Could not find contract updater')
+ let sourceText = ''
+ if (previousContract.closeTime !== contract.closeTime && contract.closeTime) {
+ sourceText = contract.closeTime.toString()
+ } else if (previousContract.question !== contract.question) {
+ sourceText = contract.question
+ }
+
+ await createCommentOrAnswerOrUpdatedContractNotification(
+ contract.id,
+ 'contract',
+ 'updated',
+ contractUpdater,
+ eventId,
+ sourceText,
+ contract
+ )
+}
+
+async function handleUnusedCommentBountyRefunds(contract: Contract) {
+ const outstandingCommentBounties = await getValues(
+ firestore.collection('txns').where('category', '==', 'COMMENT_BOUNTY')
+ )
+
+ const commentBountiesOnThisContract = sortBy(
+ outstandingCommentBounties.filter(
+ (bounty) => bounty.data?.contractId === contract.id
+ ),
+ (bounty) => bounty.createdTime
+ )
+
+ const [toBank, fromBank] = partition(
+ commentBountiesOnThisContract,
+ (bounty) => bounty.toType === 'BANK'
+ )
+ if (toBank.length <= fromBank.length) return
+
+ await firestore
+ .collection('contracts')
+ .doc(contract.id)
+ .update({ openCommentBounties: 0 })
+
+ const refunds = toBank.slice(fromBank.length)
+ await Promise.all(
+ refunds.map(async (extraBountyTxn) => {
+ const result = await firestore.runTransaction(async (trans) => {
+ const bonusTxn: TxnData = {
+ fromId: extraBountyTxn.toId,
+ fromType: 'BANK',
+ toId: extraBountyTxn.fromId,
+ toType: 'USER',
+ amount: extraBountyTxn.amount,
+ token: 'M$',
+ category: 'REFUND_COMMENT_BOUNTY',
+ data: {
+ contractId: contract.id,
+ },
+ }
+ return await runTxn(trans, bonusTxn)
+ })
+
+ if (result.status != 'success' || !result.txn) {
+ log(
+ `Couldn't refund bonus for user: ${extraBountyTxn.fromId} - status:`,
+ result.status
+ )
+ log('message:', result.message)
+ } else {
+ log(
+ `Refund bonus txn for user: ${extraBountyTxn.fromId} completed:`,
+ result.txn?.id
+ )
+ }
+ })
+ )
+}
+
+const firestore = admin.firestore()
diff --git a/functions/src/serve.ts b/functions/src/serve.ts
index 99ac6281..d861dcbc 100644
--- a/functions/src/serve.ts
+++ b/functions/src/serve.ts
@@ -29,6 +29,7 @@ import { getcurrentuser } from './get-current-user'
import { createpost } from './create-post'
import { savetwitchcredentials } from './save-twitch-credentials'
import { testscheduledfunction } from './test-scheduled-function'
+import { addcommentbounty, awardcommentbounty } from './update-comment-bounty'
type Middleware = (req: Request, res: Response, next: NextFunction) => void
const app = express()
@@ -61,6 +62,8 @@ addJsonEndpointRoute('/sellshares', sellshares)
addJsonEndpointRoute('/claimmanalink', claimmanalink)
addJsonEndpointRoute('/createmarket', createmarket)
addJsonEndpointRoute('/addliquidity', addliquidity)
+addJsonEndpointRoute('/addCommentBounty', addcommentbounty)
+addJsonEndpointRoute('/awardCommentBounty', awardcommentbounty)
addJsonEndpointRoute('/withdrawliquidity', withdrawliquidity)
addJsonEndpointRoute('/creategroup', creategroup)
addJsonEndpointRoute('/resolvemarket', resolvemarket)
diff --git a/functions/src/update-comment-bounty.ts b/functions/src/update-comment-bounty.ts
new file mode 100644
index 00000000..af1d6c0a
--- /dev/null
+++ b/functions/src/update-comment-bounty.ts
@@ -0,0 +1,162 @@
+import * as admin from 'firebase-admin'
+import { z } from 'zod'
+
+import { Contract } from '../../common/contract'
+import { User } from '../../common/user'
+import { removeUndefinedProps } from '../../common/util/object'
+import { APIError, newEndpoint, validate } from './api'
+import {
+ DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
+ HOUSE_LIQUIDITY_PROVIDER_ID,
+} from '../../common/antes'
+import { isProd } from './utils'
+import {
+ CommentBountyDepositTxn,
+ CommentBountyWithdrawalTxn,
+} from '../../common/txn'
+import { runTxn } from './transact'
+import { Comment } from '../../common/comment'
+import { createBountyNotification } from './create-notification'
+
+const bodySchema = z.object({
+ contractId: z.string(),
+ amount: z.number().gt(0),
+})
+const awardBodySchema = z.object({
+ contractId: z.string(),
+ commentId: z.string(),
+ amount: z.number().gt(0),
+})
+
+export const addcommentbounty = newEndpoint({}, async (req, auth) => {
+ const { amount, contractId } = validate(bodySchema, req.body)
+
+ if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
+
+ // run as transaction to prevent race conditions
+ return await firestore.runTransaction(async (transaction) => {
+ const userDoc = firestore.doc(`users/${auth.uid}`)
+ const userSnap = await transaction.get(userDoc)
+ if (!userSnap.exists) throw new APIError(400, 'User not found')
+ const user = userSnap.data() as User
+
+ const contractDoc = firestore.doc(`contracts/${contractId}`)
+ const contractSnap = await transaction.get(contractDoc)
+ if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
+ const contract = contractSnap.data() as Contract
+
+ if (user.balance < amount)
+ throw new APIError(400, 'Insufficient user balance')
+
+ const newCommentBountyTxn = {
+ fromId: user.id,
+ fromType: 'USER',
+ toId: isProd()
+ ? HOUSE_LIQUIDITY_PROVIDER_ID
+ : DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
+ toType: 'BANK',
+ amount,
+ token: 'M$',
+ category: 'COMMENT_BOUNTY',
+ data: {
+ contractId,
+ },
+ description: `Deposit M$${amount} from ${user.id} for comment bounty for contract ${contractId}`,
+ } as CommentBountyDepositTxn
+
+ const result = await runTxn(transaction, newCommentBountyTxn)
+
+ transaction.update(
+ contractDoc,
+ removeUndefinedProps({
+ openCommentBounties: (contract.openCommentBounties ?? 0) + amount,
+ })
+ )
+
+ return result
+ })
+})
+export const awardcommentbounty = newEndpoint({}, async (req, auth) => {
+ const { amount, commentId, contractId } = validate(awardBodySchema, req.body)
+
+ if (!isFinite(amount)) throw new APIError(400, 'Invalid amount')
+
+ // run as transaction to prevent race conditions
+ const res = await firestore.runTransaction(async (transaction) => {
+ const userDoc = firestore.doc(`users/${auth.uid}`)
+ const userSnap = await transaction.get(userDoc)
+ if (!userSnap.exists) throw new APIError(400, 'User not found')
+ const user = userSnap.data() as User
+
+ const contractDoc = firestore.doc(`contracts/${contractId}`)
+ const contractSnap = await transaction.get(contractDoc)
+ if (!contractSnap.exists) throw new APIError(400, 'Invalid contract')
+ const contract = contractSnap.data() as Contract
+
+ if (user.id !== contract.creatorId)
+ throw new APIError(
+ 400,
+ 'Only contract creator can award comment bounties'
+ )
+
+ const commentDoc = firestore.doc(
+ `contracts/${contractId}/comments/${commentId}`
+ )
+ const commentSnap = await transaction.get(commentDoc)
+ if (!commentSnap.exists) throw new APIError(400, 'Invalid comment')
+
+ const comment = commentSnap.data() as Comment
+ const amountAvailable = contract.openCommentBounties ?? 0
+ if (amountAvailable < amount)
+ throw new APIError(400, 'Insufficient open bounty balance')
+
+ const newCommentBountyTxn = {
+ fromId: isProd()
+ ? HOUSE_LIQUIDITY_PROVIDER_ID
+ : DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
+ fromType: 'BANK',
+ toId: comment.userId,
+ toType: 'USER',
+ amount,
+ token: 'M$',
+ category: 'COMMENT_BOUNTY',
+ data: {
+ contractId,
+ commentId,
+ },
+ description: `Withdrawal M$${amount} from BANK for comment ${comment.id} bounty for contract ${contractId}`,
+ } as CommentBountyWithdrawalTxn
+
+ const result = await runTxn(transaction, newCommentBountyTxn)
+
+ await transaction.update(
+ contractDoc,
+ removeUndefinedProps({
+ openCommentBounties: amountAvailable - amount,
+ })
+ )
+ await transaction.update(
+ commentDoc,
+ removeUndefinedProps({
+ bountiesAwarded: (comment.bountiesAwarded ?? 0) + amount,
+ })
+ )
+
+ return { ...result, comment, contract, user }
+ })
+ if (res.txn?.id) {
+ const { comment, contract, user } = res
+ await createBountyNotification(
+ user,
+ comment.userId,
+ amount,
+ res.txn.id,
+ contract,
+ comment.id
+ )
+ }
+
+ return res
+})
+
+const firestore = admin.firestore()
diff --git a/web/components/award-bounty-button.tsx b/web/components/award-bounty-button.tsx
new file mode 100644
index 00000000..7a69cf15
--- /dev/null
+++ b/web/components/award-bounty-button.tsx
@@ -0,0 +1,46 @@
+import clsx from 'clsx'
+import { ContractComment } from 'common/comment'
+import { useUser } from 'web/hooks/use-user'
+import { awardCommentBounty } from 'web/lib/firebase/api'
+import { track } from 'web/lib/service/analytics'
+import { Row } from './layout/row'
+import { Contract } from 'common/contract'
+import { TextButton } from 'web/components/text-button'
+import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
+import { formatMoney } from 'common/util/format'
+
+export function AwardBountyButton(prop: {
+ comment: ContractComment
+ contract: Contract
+}) {
+ const { comment, contract } = prop
+
+ const me = useUser()
+
+ const submit = () => {
+ const data = {
+ amount: COMMENT_BOUNTY_AMOUNT,
+ commentId: comment.id,
+ contractId: contract.id,
+ }
+
+ awardCommentBounty(data)
+ .then((_) => {
+ console.log('success')
+ track('award comment bounty', data)
+ })
+ .catch((reason) => console.log('Server error:', reason))
+
+ track('award comment bounty', data)
+ }
+
+ const canUp = me && me.id !== comment.userId && contract.creatorId === me.id
+ if (!canUp) return
+ return (
+
+
+ Award {formatMoney(COMMENT_BOUNTY_AMOUNT)}
+
+
+ )
+}
diff --git a/web/components/contract/add-comment-bounty.tsx b/web/components/contract/add-comment-bounty.tsx
new file mode 100644
index 00000000..8b716e71
--- /dev/null
+++ b/web/components/contract/add-comment-bounty.tsx
@@ -0,0 +1,74 @@
+import { Contract } from 'common/contract'
+import { useUser } from 'web/hooks/use-user'
+import { useState } from 'react'
+import { addCommentBounty } from 'web/lib/firebase/api'
+import { track } from 'web/lib/service/analytics'
+import { Row } from 'web/components/layout/row'
+import clsx from 'clsx'
+import { formatMoney } from 'common/util/format'
+import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
+import { Button } from 'web/components/button'
+
+export function AddCommentBountyPanel(props: { contract: Contract }) {
+ const { contract } = props
+ const { id: contractId, slug } = contract
+
+ const user = useUser()
+ const amount = COMMENT_BOUNTY_AMOUNT
+ const totalAdded = contract.openCommentBounties ?? 0
+ const [error, setError] = useState(undefined)
+ const [isSuccess, setIsSuccess] = useState(false)
+ const [isLoading, setIsLoading] = useState(false)
+
+ const submit = () => {
+ if ((user?.balance ?? 0) < amount) {
+ setError('Insufficient balance')
+ return
+ }
+
+ setIsLoading(true)
+ setIsSuccess(false)
+
+ addCommentBounty({ amount, contractId })
+ .then((_) => {
+ track('offer comment bounty', {
+ amount,
+ contractId,
+ })
+ setIsSuccess(true)
+ setError(undefined)
+ setIsLoading(false)
+ })
+ .catch((_) => setError('Server error'))
+
+ track('add comment bounty', { amount, contractId, slug })
+ }
+
+ return (
+ <>
+
+ Add a {formatMoney(amount)} bounty for good comments that the creator
+ can award.{' '}
+ {totalAdded > 0 && `(${formatMoney(totalAdded)} currently added)`}
+
+
+
+
+ Add {formatMoney(amount)} bounty
+
+ {error}
+
+
+ {isSuccess && amount && (
+ Success! Added {formatMoney(amount)} in bounties.
+ )}
+
+ {isLoading && Processing...
}
+ >
+ )
+}
diff --git a/web/components/contract/bountied-contract-badge.tsx b/web/components/contract/bountied-contract-badge.tsx
new file mode 100644
index 00000000..8e3e8c5b
--- /dev/null
+++ b/web/components/contract/bountied-contract-badge.tsx
@@ -0,0 +1,9 @@
+import { CurrencyDollarIcon } from '@heroicons/react/outline'
+
+export function BountiedContractBadge() {
+ return (
+
+ Bounty
+
+ )
+}
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx
index fc4bcfcf..22167a9c 100644
--- a/web/components/contract/contract-details.tsx
+++ b/web/components/contract/contract-details.tsx
@@ -32,6 +32,7 @@ import { PlusCircleIcon } from '@heroicons/react/solid'
import { GroupLink } from 'common/group'
import { Subtitle } from '../subtitle'
import { useIsMobile } from 'web/hooks/use-is-mobile'
+import { BountiedContractBadge } from 'web/components/contract/bountied-contract-badge'
export type ShowTime = 'resolve-date' | 'close-date'
@@ -63,6 +64,8 @@ export function MiscDetails(props: {
) : (contract?.featuredOnHomeRank ?? 0) > 0 ? (
+ ) : (contract.openCommentBounties ?? 0) > 0 ? (
+
) : volume > 0 || !isNew ? (
{formatMoney(volume)} bet
) : (
diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx
index 5187030d..df6695ed 100644
--- a/web/components/contract/contract-info-dialog.tsx
+++ b/web/components/contract/contract-info-dialog.tsx
@@ -7,7 +7,7 @@ import { capitalize } from 'lodash'
import { Contract } from 'common/contract'
import { formatMoney } from 'common/util/format'
import { contractPool, updateContract } from 'web/lib/firebase/contracts'
-import { LiquidityPanel } from '../liquidity-panel'
+import { LiquidityBountyPanel } from 'web/components/contract/liquidity-bounty-panel'
import { Col } from '../layout/col'
import { Modal } from '../layout/modal'
import { Title } from '../title'
@@ -196,9 +196,7 @@ export function ContractInfoDialog(props: {
- {contract.mechanism === 'cpmm-1' && !contract.resolution && (
-
- )}
+ {!contract.resolution && }
>
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index 33a3c05a..e53881d3 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -5,7 +5,7 @@ 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 { groupBy, sortBy } from 'lodash'
+import { groupBy, sortBy, sum } from 'lodash'
import { Bet } from 'common/bet'
import { Contract } from 'common/contract'
import { PAST_BETS } from 'common/user'
@@ -25,6 +25,13 @@ import {
import { buildArray } from 'common/util/array'
import { ContractComment } from 'common/comment'
+import { formatMoney } from 'common/util/format'
+import { Button } from 'web/components/button'
+import { MINUTE_MS } from 'common/util/time'
+import { useUser } from 'web/hooks/use-user'
+import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
+import { Tooltip } from 'web/components/tooltip'
+
export function ContractTabs(props: {
contract: Contract
bets: Bet[]
@@ -32,6 +39,7 @@ export function ContractTabs(props: {
comments: ContractComment[]
}) {
const { contract, bets, userBets, comments } = props
+ const { openCommentBounties } = contract
const yourTrades = (
@@ -43,8 +51,16 @@ export function ContractTabs(props: {
const tabs = buildArray(
{
- title: 'Comments',
+ title: `Comments`,
+ tooltip: openCommentBounties
+ ? `The creator of this market may award ${formatMoney(
+ COMMENT_BOUNTY_AMOUNT
+ )} for good comments. ${formatMoney(
+ openCommentBounties
+ )} currently available.`
+ : undefined,
content:
,
+ inlineTabIcon:
({formatMoney(COMMENT_BOUNTY_AMOUNT)}) ,
},
{
title: capitalize(PAST_BETS),
@@ -68,6 +84,8 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
const { contract } = props
const tips = useTipTxns({ contractId: contract.id })
const comments = useComments(contract.id) ?? props.comments
+ const [sort, setSort] = useState<'Newest' | 'Best'>('Best')
+ const me = useUser()
if (comments == null) {
return
}
@@ -119,12 +137,44 @@ const CommentsTabContent = memo(function CommentsTabContent(props: {
>
)
} else {
- const commentsByParent = groupBy(comments, (c) => c.replyToCommentId ?? '_')
+ const tipsOrBountiesAwarded =
+ Object.keys(tips).length > 0 || comments.some((c) => c.bountiesAwarded)
+
+ const commentsByParent = groupBy(
+ sortBy(comments, (c) =>
+ sort === 'Newest'
+ ? -c.createdTime
+ : // Is this too magic? If there are tips/bounties, 'Best' shows your own comments made within the last 10 minutes first, then sorts by score
+ tipsOrBountiesAwarded &&
+ c.createdTime > Date.now() - 10 * MINUTE_MS &&
+ c.userId === me?.id
+ ? -Infinity
+ : -((c.bountiesAwarded ?? 0) + sum(Object.values(tips[c.id] ?? [])))
+ ),
+ (c) => c.replyToCommentId ?? '_'
+ )
+
const topLevelComments = commentsByParent['_'] ?? []
return (
<>
+
setSort(sort === 'Newest' ? 'Best' : 'Newest')}
+ >
+
+ Sorted by: {sort}
+
+
- {sortBy(topLevelComments, (c) => -c.createdTime).map((parent) => (
+ {topLevelComments.map((parent) => (
>
-
return (
,
- },
- showWithdrawal && {
- title: 'Withdraw',
- content: (
-
- ),
- },
{
- title: 'Pool',
- content: ,
- }
+ title: 'Bounty Comments',
+ content: ,
+ },
+ (isCreator || isAdmin) &&
+ isCPMM && {
+ title: (isAdmin ? '[Admin] ' : '') + 'Subsidize',
+ content: ,
+ },
+ showWithdrawal &&
+ isCPMM && {
+ title: 'Withdraw',
+ content: (
+
+ ),
+ },
+
+ (isCreator || isAdmin) &&
+ isCPMM && {
+ title: 'Pool',
+ content: ,
+ }
)}
/>
)
diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx
index 1b62690b..20d124f8 100644
--- a/web/components/feed/feed-comments.tsx
+++ b/web/components/feed/feed-comments.tsx
@@ -19,6 +19,7 @@ import { Content } from '../editor'
import { Editor } from '@tiptap/react'
import { UserLink } from 'web/components/user-link'
import { CommentInput } from '../comment-input'
+import { AwardBountyButton } from 'web/components/award-bounty-button'
export type ReplyTo = { id: string; username: string }
@@ -85,6 +86,7 @@ export function FeedComment(props: {
commenterPositionShares,
commenterPositionOutcome,
createdTime,
+ bountiesAwarded,
} = comment
const betOutcome = comment.betOutcome
let bought: string | undefined
@@ -93,6 +95,7 @@ export function FeedComment(props: {
bought = comment.betAmount >= 0 ? 'bought' : 'sold'
money = formatMoney(Math.abs(comment.betAmount))
}
+ const totalAwarded = bountiesAwarded ?? 0
const router = useRouter()
const highlighted = router.asPath.endsWith(`#${comment.id}`)
@@ -162,6 +165,11 @@ export function FeedComment(props: {
createdTime={createdTime}
elementId={comment.id}
/>
+ {totalAwarded > 0 && (
+
+ +{formatMoney(totalAwarded)}
+
+ )}
{tips && }
+ {(contract.openCommentBounties ?? 0) > 0 && (
+
+ )}
{onReplyClick && (
void
}) {
const user = useUser()
+ const { contract, parentAnswerOutcome, parentCommentId, replyTo, className } =
+ props
+ const { openCommentBounties } = contract
async function onSubmitComment(editor: Editor) {
if (!user) {
track('sign in to comment')
return await firebaseLogin()
}
await createCommentOnContract(
- props.contract.id,
+ contract.id,
editor.getJSON(),
user,
- props.parentAnswerOutcome,
- props.parentCommentId
+ !!openCommentBounties,
+ parentAnswerOutcome,
+ parentCommentId
)
props.onSubmitComment?.()
}
return (
)
}
diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx
index b82131ec..deff2203 100644
--- a/web/components/layout/tabs.tsx
+++ b/web/components/layout/tabs.tsx
@@ -3,13 +3,15 @@ import { useRouter, NextRouter } from 'next/router'
import { ReactNode, useState } from 'react'
import { track } from '@amplitude/analytics-browser'
import { Col } from './col'
+import { Tooltip } from 'web/components/tooltip'
+import { Row } from 'web/components/layout/row'
type Tab = {
title: string
- tabIcon?: ReactNode
content: ReactNode
- // If set, show a badge with this content
- badge?: string
+ stackedTabIcon?: ReactNode
+ inlineTabIcon?: ReactNode
+ tooltip?: string
}
type TabProps = {
@@ -56,12 +58,16 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) {
)}
aria-current={activeIndex === i ? 'page' : undefined}
>
- {tab.badge ? (
- {tab.badge}
- ) : null}
- {tab.tabIcon && {tab.tabIcon}
}
- {tab.title}
+
+ {tab.stackedTabIcon && (
+ {tab.stackedTabIcon}
+ )}
+
+ {tab.title}
+ {tab.inlineTabIcon}
+
+
))}
diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx
index ccb8361f..1dcb0f05 100644
--- a/web/components/tipper.tsx
+++ b/web/components/tipper.tsx
@@ -116,7 +116,7 @@ function DownTip(props: { onClick?: () => void }) {
noTap
>
@@ -137,7 +137,7 @@ function UpTip(props: { onClick?: () => void; value: number }) {
noTap
>
diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx
index 623b4d35..f9f77cf6 100644
--- a/web/components/user-page.tsx
+++ b/web/components/user-page.tsx
@@ -192,7 +192,7 @@ export function UserPage(props: { user: User }) {
tabs={[
{
title: 'Markets',
- tabIcon: ,
+ stackedTabIcon: ,
content: (
<>
@@ -202,7 +202,7 @@ export function UserPage(props: { user: User }) {
},
{
title: 'Portfolio',
- tabIcon: ,
+ stackedTabIcon: ,
content: (
<>
@@ -214,7 +214,7 @@ export function UserPage(props: { user: User }) {
},
{
title: 'Comments',
- tabIcon: ,
+ stackedTabIcon: ,
content: (
<>
diff --git a/web/lib/firebase/api.ts b/web/lib/firebase/api.ts
index 8aa7a067..3e803bc6 100644
--- a/web/lib/firebase/api.ts
+++ b/web/lib/firebase/api.ts
@@ -46,6 +46,14 @@ export function addLiquidity(params: any) {
return call(getFunctionUrl('addliquidity'), 'POST', params)
}
+export function addCommentBounty(params: any) {
+ return call(getFunctionUrl('addcommentbounty'), 'POST', params)
+}
+
+export function awardCommentBounty(params: any) {
+ return call(getFunctionUrl('awardcommentbounty'), 'POST', params)
+}
+
export function withdrawLiquidity(params: any) {
return call(getFunctionUrl('withdrawliquidity'), 'POST', params)
}
diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts
index 733a1e06..e1b4ccef 100644
--- a/web/lib/firebase/comments.ts
+++ b/web/lib/firebase/comments.ts
@@ -35,6 +35,7 @@ export async function createCommentOnContract(
contractId: string,
content: JSONContent,
user: User,
+ onContractWithBounty: boolean,
answerOutcome?: string,
replyToCommentId?: string
) {
@@ -50,7 +51,8 @@ export async function createCommentOnContract(
content,
user,
ref,
- replyToCommentId
+ replyToCommentId,
+ onContractWithBounty
)
}
export async function createCommentOnGroup(
@@ -95,7 +97,8 @@ async function createComment(
content: JSONContent,
user: User,
ref: DocumentReference,
- replyToCommentId?: string
+ replyToCommentId?: string,
+ onContractWithBounty?: boolean
) {
const comment = removeUndefinedProps({
id: ref.id,
@@ -108,13 +111,19 @@ async function createComment(
replyToCommentId: replyToCommentId,
...extraFields,
})
-
- track(`${extraFields.commentType} message`, {
- user,
- commentId: ref.id,
- surfaceId,
- replyToCommentId: replyToCommentId,
- })
+ track(
+ `${extraFields.commentType} message`,
+ removeUndefinedProps({
+ user,
+ commentId: ref.id,
+ surfaceId,
+ replyToCommentId: replyToCommentId,
+ onContractWithBounty:
+ extraFields.commentType === 'contract'
+ ? onContractWithBounty
+ : undefined,
+ })
+ )
return await setDoc(ref, comment)
}
diff --git a/web/package.json b/web/package.json
index a3ec9aaa..a5fa8ced 100644
--- a/web/package.json
+++ b/web/package.json
@@ -22,7 +22,7 @@
"@amplitude/analytics-browser": "0.4.1",
"@floating-ui/react-dom-interactions": "0.9.2",
"@headlessui/react": "1.6.1",
- "@heroicons/react": "1.0.5",
+ "@heroicons/react": "1.0.6",
"@nivo/core": "0.80.0",
"@nivo/line": "0.80.0",
"@nivo/tooltip": "0.80.0",
From 31de3636fdcb09437ad47dae33bfc7402f8783b5 Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Fri, 30 Sep 2022 09:34:58 -0600
Subject: [PATCH 017/101] Fix comment tab title
---
web/components/contract/contract-tabs.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index e53881d3..9b732224 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -60,7 +60,9 @@ export function ContractTabs(props: {
)} currently available.`
: undefined,
content: ,
- inlineTabIcon: ({formatMoney(COMMENT_BOUNTY_AMOUNT)}) ,
+ inlineTabIcon: openCommentBounties ? (
+ ({formatMoney(COMMENT_BOUNTY_AMOUNT)})
+ ) : undefined,
},
{
title: capitalize(PAST_BETS),
From 3677de58c3131d317c6ea0ce8097215ccf8f6dee Mon Sep 17 00:00:00 2001
From: Ian Philips
Date: Fri, 30 Sep 2022 10:00:55 -0600
Subject: [PATCH 018/101] Add tooltip and badge on contract for bounties
---
.../contract/bountied-contract-badge.tsx | 25 +++++++++++++++++++
web/components/contract/contract-details.tsx | 19 +++++++++-----
web/components/contract/contract-tabs.tsx | 7 ++----
3 files changed, 40 insertions(+), 11 deletions(-)
diff --git a/web/components/contract/bountied-contract-badge.tsx b/web/components/contract/bountied-contract-badge.tsx
index 8e3e8c5b..3e1ed68c 100644
--- a/web/components/contract/bountied-contract-badge.tsx
+++ b/web/components/contract/bountied-contract-badge.tsx
@@ -1,4 +1,8 @@
import { CurrencyDollarIcon } from '@heroicons/react/outline'
+import { Contract } from 'common/contract'
+import { Tooltip } from 'web/components/tooltip'
+import { formatMoney } from 'common/lib/util/format'
+import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
export function BountiedContractBadge() {
return (
@@ -7,3 +11,24 @@ export function BountiedContractBadge() {
)
}
+
+export function BountiedContractSmallBadge(props: { contract: Contract }) {
+ const { contract } = props
+ const { openCommentBounties } = contract
+ if (!openCommentBounties) return
+
+ return (
+
+
+ Bountied Comments
+
+
+ )
+}
+
+export const CommentBountiesTooltipText = (openCommentBounties: number) =>
+ `The creator of this market may award ${formatMoney(
+ COMMENT_BOUNTY_AMOUNT
+ )} for good comments. ${formatMoney(
+ openCommentBounties
+ )} currently available.`
diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx
index 22167a9c..7c84fadf 100644
--- a/web/components/contract/contract-details.tsx
+++ b/web/components/contract/contract-details.tsx
@@ -32,7 +32,10 @@ import { PlusCircleIcon } from '@heroicons/react/solid'
import { GroupLink } from 'common/group'
import { Subtitle } from '../subtitle'
import { useIsMobile } from 'web/hooks/use-is-mobile'
-import { BountiedContractBadge } from 'web/components/contract/bountied-contract-badge'
+import {
+ BountiedContractBadge,
+ BountiedContractSmallBadge,
+} from 'web/components/contract/bountied-contract-badge'
export type ShowTime = 'resolve-date' | 'close-date'
@@ -129,9 +132,10 @@ export function ContractDetails(props: {
{/* GROUPS */}
{isMobile && (
-
+
+
-
+
)}
)
@@ -181,7 +185,10 @@ export function MarketSubheader(props: {
isCreator={isCreator}
/>
{!isMobile && (
-
+
+
+
+
)}
@@ -328,14 +335,14 @@ export function GroupDisplay(props: { groupToDisplay?: GroupLink | null }) {
if (groupToDisplay) {
return (
-
+
{groupToDisplay.name}
)
} else
return (
-
+
No Group
)
diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx
index 9b732224..d29806b5 100644
--- a/web/components/contract/contract-tabs.tsx
+++ b/web/components/contract/contract-tabs.tsx
@@ -31,6 +31,7 @@ import { MINUTE_MS } from 'common/util/time'
import { useUser } from 'web/hooks/use-user'
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
import { Tooltip } from 'web/components/tooltip'
+import { CommentBountiesTooltipText } from 'web/components/contract/bountied-contract-badge'
export function ContractTabs(props: {
contract: Contract
@@ -53,11 +54,7 @@ export function ContractTabs(props: {
{
title: `Comments`,
tooltip: openCommentBounties
- ? `The creator of this market may award ${formatMoney(
- COMMENT_BOUNTY_AMOUNT
- )} for good comments. ${formatMoney(
- openCommentBounties
- )} currently available.`
+ ? CommentBountiesTooltipText(openCommentBounties)
: undefined,
content:
,
inlineTabIcon: openCommentBounties ? (
From ab883ea7777a9d8428a7a467d6fe6c5eae3ee77d Mon Sep 17 00:00:00 2001
From: James Grugett
Date: Fri, 30 Sep 2022 12:00:14 -0500
Subject: [PATCH 019/101] Order home group sections by daily score.
---
web/pages/home/edit.tsx | 16 ++-----
web/pages/home/index.tsx | 93 ++++++++++++++++++++++------------------
2 files changed, 56 insertions(+), 53 deletions(-)
diff --git a/web/pages/home/edit.tsx b/web/pages/home/edit.tsx
index 8c5f8ab5..0b496757 100644
--- a/web/pages/home/edit.tsx
+++ b/web/pages/home/edit.tsx
@@ -7,12 +7,11 @@ import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page'
import { SiteLink } from 'web/components/site-link'
import { Title } from 'web/components/title'
-import { useMemberGroupsSubscription } from 'web/hooks/use-group'
import { useTracking } from 'web/hooks/use-tracking'
import { useUser } from 'web/hooks/use-user'
import { updateUser } from 'web/lib/firebase/users'
import { track } from 'web/lib/service/analytics'
-import { getHomeItems, TrendingGroupsSection } from '.'
+import { getHomeItems } from '.'
export default function Home() {
const user = useUser()
@@ -27,8 +26,7 @@ export default function Home() {
setHomeSections(newHomeSections)
}
- const groups = useMemberGroupsSubscription(user)
- const { sections } = getHomeItems(groups ?? [], homeSections)
+ const { sections } = getHomeItems(homeSections)
return (
@@ -38,14 +36,8 @@ export default function Home() {
-
-
-
-
-
+
+
diff --git a/web/pages/home/index.tsx b/web/pages/home/index.tsx
index e920b20f..de5e95f2 100644
--- a/web/pages/home/index.tsx
+++ b/web/pages/home/index.tsx
@@ -8,7 +8,7 @@ import {
import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline'
import clsx from 'clsx'
import { toast, Toaster } from 'react-hot-toast'
-import { Dictionary } from 'lodash'
+import { Dictionary, sortBy, sum } from 'lodash'
import { Page } from 'web/components/page'
import { Col } from 'web/components/layout/col'
@@ -62,16 +62,16 @@ export default function Home() {
}
})
- const groups = useMemberGroupsSubscription(user)
-
- const { sections } = getHomeItems(groups ?? [], user?.homeSections ?? [])
+ const { sections } = getHomeItems(user?.homeSections ?? [])
useEffect(() => {
- if (user && !user.homeSections && sections.length > 0 && groups) {
+ if (user && !user.homeSections && sections.length > 0) {
// Save initial home sections.
updateUser(user.id, { homeSections: sections.map((s) => s.id) })
}
- }, [user, sections, groups])
+ }, [user, sections])
+
+ const groups = useMemberGroupsSubscription(user)
const groupContracts = useContractsByDailyScoreGroups(
groups?.map((g) => g.slug)
@@ -94,14 +94,15 @@ export default function Home() {
) : (
<>
- {sections.map((section) =>
- renderSection(section, user, groups, groupContracts)
- )}
+ {sections.map((section) => renderSection(section, user))}
+
+ {renderGroupSections(user, groups, groupContracts)}
>
)}
+
{
+export const getHomeItems = (sections: string[]) => {
// Accommodate old home sections.
if (!isArray(sections)) sections = []
const items: { id: string; label: string; group?: Group }[] = [
...HOME_SECTIONS,
- ...groups.map((g) => ({
- label: g.name,
- id: g.id,
- group: g,
- })),
]
const itemsById = keyBy(items, 'id')
@@ -152,12 +148,7 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
}
}
-function renderSection(
- section: { id: string; label: string },
- user: User,
- groups: Group[] | undefined,
- groupContracts: Dictionary | undefined
-) {
+function renderSection(section: { id: string; label: string }, user: User) {
const { id, label } = section
if (id === 'daily-movers') {
return
@@ -178,25 +169,47 @@ function renderSection(
)
- if (groups && groupContracts) {
- const group = groups.find((g) => g.id === id)
- if (group) {
- const contracts = groupContracts[group.slug].filter(
- (c) => Math.abs(c.probChanges.day) >= 0.01
- )
- if (contracts.length === 0) return null
- return (
-
- )
- }
+ return null
+}
+
+function renderGroupSections(
+ user: User,
+ groups: Group[] | undefined,
+ groupContracts: Dictionary | undefined
+) {
+ if (!groups || !groupContracts) {
+ return
}
- return null
+ const filteredGroups = groups.filter((g) => groupContracts[g.slug])
+ const orderedGroups = sortBy(filteredGroups, (g) =>
+ // Sort by sum of top two daily scores.
+ sum(
+ sortBy(groupContracts[g.slug].map((c) => c.dailyScore))
+ .reverse()
+ .slice(0, 2)
+ )
+ ).reverse()
+
+ return (
+ <>
+ {orderedGroups.map((group) => {
+ const contracts = groupContracts[group.slug].filter(
+ (c) => Math.abs(c.probChanges.day) >= 0.01
+ )
+ if (contracts.length === 0) return null
+
+ return (
+
+ )
+ })}
+ >
+ )
}
function SectionHeader(props: {
@@ -371,9 +384,7 @@ export function TrendingGroupsSection(props: {
return (
-
- {!full && }
-
+
{chosenGroups.map((g) => (
Date: Fri, 30 Sep 2022 13:22:10 -0500
Subject: [PATCH 020/101] Fix import
---
web/components/contract/bountied-contract-badge.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/web/components/contract/bountied-contract-badge.tsx b/web/components/contract/bountied-contract-badge.tsx
index 3e1ed68c..b3e230cb 100644
--- a/web/components/contract/bountied-contract-badge.tsx
+++ b/web/components/contract/bountied-contract-badge.tsx
@@ -1,7 +1,7 @@
import { CurrencyDollarIcon } from '@heroicons/react/outline'
import { Contract } from 'common/contract'
import { Tooltip } from 'web/components/tooltip'
-import { formatMoney } from 'common/lib/util/format'
+import { formatMoney } from 'common/util/format'
import { COMMENT_BOUNTY_AMOUNT } from 'common/economy'
export function BountiedContractBadge() {
From b2f81c11496a54cd86b6b87144952ac4bb98272a Mon Sep 17 00:00:00 2001
From: Phil
Date: Fri, 30 Sep 2022 20:01:51 +0100
Subject: [PATCH 021/101] Twitch minor fix (#973)
* Made Twitch copy link buttons links so right-click -> copy URL works.
* Added Twitch OBS screenshot to public folder.
---
web/pages/twitch.tsx | 120 +++++++++++++----------
web/public/twitch-bot-obs-screenshot.jpg | Bin 0 -> 130637 bytes
2 files changed, 70 insertions(+), 50 deletions(-)
create mode 100644 web/public/twitch-bot-obs-screenshot.jpg
diff --git a/web/pages/twitch.tsx b/web/pages/twitch.tsx
index 6508a69e..2331fa1c 100644
--- a/web/pages/twitch.tsx
+++ b/web/pages/twitch.tsx
@@ -257,6 +257,30 @@ function BotSetupStep(props: {
)
}
+function CopyLinkButton(props: { link: string; text: string }) {
+ const { link, text } = props
+ const toastTheme = {
+ className: '!bg-primary !text-white',
+ icon: ,
+ }
+ const copyLinkCallback = async () => {
+ copyToClipboard(link)
+ toast.success(text + ' copied', toastTheme)
+ }
+ return (
+ e.preventDefault()}>
+
+ {text}
+
+
+ )
+}
+
function BotConnectButton(props: {
privateUser: PrivateUser | null | undefined
}) {
@@ -338,39 +362,67 @@ function SetUpBot(props: {
}) {
const { user, privateUser } = props
const twitchLinked =
+ privateUser &&
privateUser?.twitchInfo?.twitchName &&
!privateUser?.twitchInfo?.needsRelinking
? true
: undefined
- const toastTheme = {
- className: '!bg-primary !text-white',
- icon: ,
- }
- const copyOverlayLink = async () => {
- if (!privateUser) return
- copyToClipboard(getOverlayURLForUser(privateUser))
- toast.success('Overlay link copied!', toastTheme)
- }
- const copyDockLink = async () => {
- if (!privateUser) return
- copyToClipboard(getDockURLForUser(privateUser))
- toast.success('Dock link copied!', toastTheme)
- }
return (
<>
To add the bot to your stream make sure you have logged in then follow
the steps below.
- {!twitchLinked && (
+ {twitchLinked && privateUser ? (
+
+
+ }
+ >
+ Use the button above to add the bot to your channel. Then mod it
+ by typing in your Twitch chat: /mod ManifoldBot
+
+ If the bot is not modded it will not be able to respond to
+ commands properly.
+
+
+ }
+ >
+ Create a new browser source in your streaming software such as
+ OBS. Paste in the above link and type in the desired size. We
+ recommend 450x375.
+
+
+ }
+ >
+ The bot can be controlled entirely through chat. But we made an
+ easy to use control panel. Share the link with your mods or embed
+ it into your OBS as a custom dock.
+
+
+ ) : (
)}
-
-
- }
- >
- Use the button above to add the bot to your channel. Then mod it by
- typing in your Twitch chat: /mod ManifoldBot
-
- If the bot is not modded it will not be able to respond to commands
- properly.
-
-
- Create a new browser source in your streaming software such as OBS.
- Paste in the above link and type in the desired size. We recommend
- 450x375.
-
-
- The bot can be controlled entirely through chat. But we made an easy
- to use control panel. Share the link with your mods or embed it into
- your OBS as a custom dock.
-
-