Flag incorrectly resolved markets, warn about unreliable creators (#945)
* Flag incorrectly resolved markets, warn about unreliable creators * Address James' review nits * Added a loading state and some copy-changes * Fix missing refactor * Fix vercel error * Fix merging issues
This commit is contained in:
parent
3fb43c16c4
commit
06571a3657
|
@ -62,6 +62,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
featuredOnHomeRank?: number
|
featuredOnHomeRank?: number
|
||||||
likedByUserIds?: string[]
|
likedByUserIds?: string[]
|
||||||
likedByUserCount?: number
|
likedByUserCount?: number
|
||||||
|
flaggedByUsernames?: string[]
|
||||||
openCommentBounties?: number
|
openCommentBounties?: number
|
||||||
} & T
|
} & T
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@ export type User = {
|
||||||
allTime: number
|
allTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fractionResolvedCorrectly: number
|
||||||
|
|
||||||
nextLoanCached: number
|
nextLoanCached: number
|
||||||
followerCountCached: number
|
followerCountCached: number
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
followerCountCached: 0,
|
followerCountCached: 0,
|
||||||
followedCategories: DEFAULT_CATEGORIES,
|
followedCategories: DEFAULT_CATEGORIES,
|
||||||
shouldShowWelcome: true,
|
shouldShowWelcome: true,
|
||||||
|
fractionResolvedCorrectly: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('users').doc(auth.uid).create(user)
|
await firestore.collection('users').doc(auth.uid).create(user)
|
||||||
|
|
|
@ -135,6 +135,28 @@ export async function updateMetricsCore() {
|
||||||
lastPortfolio.investmentValue !== newPortfolio.investmentValue
|
lastPortfolio.investmentValue !== newPortfolio.investmentValue
|
||||||
|
|
||||||
const newProfit = calculateNewProfit(portfolioHistory, newPortfolio)
|
const newProfit = calculateNewProfit(portfolioHistory, newPortfolio)
|
||||||
|
const contractRatios = userContracts
|
||||||
|
.map((contract) => {
|
||||||
|
if (
|
||||||
|
!contract.flaggedByUsernames ||
|
||||||
|
contract.flaggedByUsernames?.length === 0
|
||||||
|
) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const contractRatio =
|
||||||
|
contract.flaggedByUsernames.length / (contract.uniqueBettorCount ?? 1)
|
||||||
|
|
||||||
|
return contractRatio
|
||||||
|
})
|
||||||
|
.filter((ratio) => ratio > 0)
|
||||||
|
const badResolutions = contractRatios.filter(
|
||||||
|
(ratio) => ratio > BAD_RESOLUTION_THRESHOLD
|
||||||
|
)
|
||||||
|
let newFractionResolvedCorrectly = 0
|
||||||
|
if (userContracts.length > 0) {
|
||||||
|
newFractionResolvedCorrectly =
|
||||||
|
(userContracts.length - badResolutions.length) / userContracts.length
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
@ -142,6 +164,7 @@ export async function updateMetricsCore() {
|
||||||
newPortfolio,
|
newPortfolio,
|
||||||
newProfit,
|
newProfit,
|
||||||
didPortfolioChange,
|
didPortfolioChange,
|
||||||
|
newFractionResolvedCorrectly,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -163,6 +186,7 @@ export async function updateMetricsCore() {
|
||||||
newPortfolio,
|
newPortfolio,
|
||||||
newProfit,
|
newProfit,
|
||||||
didPortfolioChange,
|
didPortfolioChange,
|
||||||
|
newFractionResolvedCorrectly,
|
||||||
}) => {
|
}) => {
|
||||||
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
|
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
|
||||||
return {
|
return {
|
||||||
|
@ -172,6 +196,7 @@ export async function updateMetricsCore() {
|
||||||
creatorVolumeCached: newCreatorVolume,
|
creatorVolumeCached: newCreatorVolume,
|
||||||
profitCached: newProfit,
|
profitCached: newProfit,
|
||||||
nextLoanCached,
|
nextLoanCached,
|
||||||
|
fractionResolvedCorrectly: newFractionResolvedCorrectly,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -243,3 +268,5 @@ const topUserScores = (scores: { [userId: string]: number }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupContractDoc = { contractId: string; createdTime: number }
|
type GroupContractDoc = { contractId: string; createdTime: number }
|
||||||
|
|
||||||
|
const BAD_RESOLUTION_THRESHOLD = 0.1
|
||||||
|
|
|
@ -26,6 +26,7 @@ export function ConfirmationButton(props: {
|
||||||
onSubmit?: () => void
|
onSubmit?: () => void
|
||||||
onOpenChanged?: (isOpen: boolean) => void
|
onOpenChanged?: (isOpen: boolean) => void
|
||||||
onSubmitWithSuccess?: () => Promise<boolean>
|
onSubmitWithSuccess?: () => Promise<boolean>
|
||||||
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
openModalBtn,
|
openModalBtn,
|
||||||
|
@ -35,6 +36,7 @@ export function ConfirmationButton(props: {
|
||||||
children,
|
children,
|
||||||
onOpenChanged,
|
onOpenChanged,
|
||||||
onSubmitWithSuccess,
|
onSubmitWithSuccess,
|
||||||
|
disabled,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
@ -72,9 +74,15 @@ export function ConfirmationButton(props: {
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={clsx(openModalBtn.className)}
|
className={openModalBtn.className}
|
||||||
onClick={() => updateOpen(true)}
|
onClick={() => {
|
||||||
|
if (disabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateOpen(true)
|
||||||
|
}}
|
||||||
disabled={openModalBtn.disabled}
|
disabled={openModalBtn.disabled}
|
||||||
color={openModalBtn.color}
|
color={openModalBtn.color}
|
||||||
size={openModalBtn.size}
|
size={openModalBtn.size}
|
||||||
|
|
|
@ -218,17 +218,19 @@ export function BinaryResolutionOrChance(props: {
|
||||||
className={clsx('items-end', large ? 'text-4xl' : 'text-3xl', className)}
|
className={clsx('items-end', large ? 'text-4xl' : 'text-3xl', className)}
|
||||||
>
|
>
|
||||||
{resolution ? (
|
{resolution ? (
|
||||||
<>
|
<Row className="flex items-start">
|
||||||
<div
|
<div>
|
||||||
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
|
<div
|
||||||
>
|
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
|
||||||
Resolved
|
>
|
||||||
|
Resolved
|
||||||
|
</div>
|
||||||
|
<BinaryContractOutcomeLabel
|
||||||
|
contract={contract}
|
||||||
|
resolution={resolution}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<BinaryContractOutcomeLabel
|
</Row>
|
||||||
contract={contract}
|
|
||||||
resolution={resolution}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{probAfter && probChanged ? (
|
{probAfter && probChanged ? (
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { useState } from 'react'
|
||||||
import NewContractBadge from '../new-contract-badge'
|
import NewContractBadge from '../new-contract-badge'
|
||||||
import { MiniUserFollowButton } from '../follow-button'
|
import { MiniUserFollowButton } from '../follow-button'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser, useUserById } from 'web/hooks/use-user'
|
||||||
import { exhibitExts } from 'common/util/parse'
|
import { exhibitExts } from 'common/util/parse'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { Modal } from 'web/components/layout/modal'
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
@ -28,7 +28,7 @@ import { UserLink } from 'web/components/user-link'
|
||||||
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge'
|
||||||
import { Tooltip } from 'web/components/tooltip'
|
import { Tooltip } from 'web/components/tooltip'
|
||||||
import { ExtraContractActionsRow } from './extra-contract-actions-row'
|
import { ExtraContractActionsRow } from './extra-contract-actions-row'
|
||||||
import { PlusCircleIcon } from '@heroicons/react/solid'
|
import { ExclamationIcon, PlusCircleIcon } from '@heroicons/react/solid'
|
||||||
import { GroupLink } from 'common/group'
|
import { GroupLink } from 'common/group'
|
||||||
import { Subtitle } from '../subtitle'
|
import { Subtitle } from '../subtitle'
|
||||||
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
import { useIsMobile } from 'web/hooks/use-is-mobile'
|
||||||
|
@ -149,6 +149,8 @@ export function MarketSubheader(props: {
|
||||||
const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract
|
const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract
|
||||||
const { resolvedDate } = contractMetrics(contract)
|
const { resolvedDate } = contractMetrics(contract)
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const correctResolutionPercentage =
|
||||||
|
useUserById(creatorId)?.fractionResolvedCorrectly
|
||||||
const isCreator = user?.id === creatorId
|
const isCreator = user?.id === creatorId
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
return (
|
return (
|
||||||
|
@ -160,13 +162,14 @@ export function MarketSubheader(props: {
|
||||||
size={9}
|
size={9}
|
||||||
className="mr-1.5"
|
className="mr-1.5"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!disabled && (
|
{!disabled && (
|
||||||
<div className="absolute mt-3 ml-[11px]">
|
<div className="absolute mt-3 ml-[11px]">
|
||||||
<MiniUserFollowButton userId={creatorId} />
|
<MiniUserFollowButton userId={creatorId} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Col className="text-greyscale-6 ml-2 flex-1 flex-wrap text-sm">
|
<Col className="text-greyscale-6 ml-2 flex-1 flex-wrap text-sm">
|
||||||
<Row className="w-full justify-between ">
|
<Row className="w-full space-x-1 ">
|
||||||
{disabled ? (
|
{disabled ? (
|
||||||
creatorName
|
creatorName
|
||||||
) : (
|
) : (
|
||||||
|
@ -177,6 +180,12 @@ export function MarketSubheader(props: {
|
||||||
short={isMobile}
|
short={isMobile}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{correctResolutionPercentage != null &&
|
||||||
|
correctResolutionPercentage < BAD_CREATOR_THRESHOLD && (
|
||||||
|
<Tooltip text="This creator has a track record of creating contracts that are resolved incorrectly.">
|
||||||
|
<ExclamationIcon className="h-6 w-6 text-yellow-500" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="text-2xs text-greyscale-4 flex-wrap gap-2 sm:text-xs">
|
<Row className="text-2xs text-greyscale-4 flex-wrap gap-2 sm:text-xs">
|
||||||
<CloseOrResolveTime
|
<CloseOrResolveTime
|
||||||
|
@ -487,3 +496,5 @@ function EditableCloseDate(props: {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BAD_CREATOR_THRESHOLD = 0.8
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
} from 'common/contract'
|
} from 'common/contract'
|
||||||
import { ContractDetails } from './contract-details'
|
import { ContractDetails } from './contract-details'
|
||||||
|
import { ContractReportResolution } from './contract-report-resolution'
|
||||||
|
|
||||||
const OverviewQuestion = (props: { text: string }) => (
|
const OverviewQuestion = (props: { text: string }) => (
|
||||||
<Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} />
|
<Linkify className="text-lg text-indigo-700 sm:text-2xl" text={props.text} />
|
||||||
|
@ -114,7 +115,16 @@ const BinaryOverview = (props: { contract: BinaryContract; bets: Bet[] }) => {
|
||||||
<ContractDetails contract={contract} />
|
<ContractDetails contract={contract} />
|
||||||
<Row className="justify-between gap-4">
|
<Row className="justify-between gap-4">
|
||||||
<OverviewQuestion text={contract.question} />
|
<OverviewQuestion text={contract.question} />
|
||||||
<BinaryResolutionOrChance contract={contract} large />
|
<Row>
|
||||||
|
<BinaryResolutionOrChance
|
||||||
|
className="flex items-end"
|
||||||
|
contract={contract}
|
||||||
|
large
|
||||||
|
/>
|
||||||
|
{contract.isResolved && (
|
||||||
|
<ContractReportResolution contract={contract} />
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<SizedContractChart
|
<SizedContractChart
|
||||||
|
@ -144,7 +154,13 @@ const ChoiceOverview = (props: {
|
||||||
<ContractDetails contract={contract} />
|
<ContractDetails contract={contract} />
|
||||||
<OverviewQuestion text={question} />
|
<OverviewQuestion text={question} />
|
||||||
{resolution && (
|
{resolution && (
|
||||||
<FreeResponseResolutionOrChance contract={contract} truncate="none" />
|
<Row>
|
||||||
|
<FreeResponseResolutionOrChance
|
||||||
|
contract={contract}
|
||||||
|
truncate="none"
|
||||||
|
/>
|
||||||
|
<ContractReportResolution contract={contract} />
|
||||||
|
</Row>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<SizedContractChart
|
<SizedContractChart
|
||||||
|
|
77
web/components/contract/contract-report-resolution.tsx
Normal file
77
web/components/contract/contract-report-resolution.tsx
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { updateContract } from 'web/lib/firebase/contracts'
|
||||||
|
import { Tooltip } from '../tooltip'
|
||||||
|
import { ConfirmationButton } from '../confirmation-button'
|
||||||
|
import { Row } from '../layout/row'
|
||||||
|
import { FlagIcon } from '@heroicons/react/solid'
|
||||||
|
import { buildArray } from 'common/util/array'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function ContractReportResolution(props: { contract: Contract }) {
|
||||||
|
const { contract } = props
|
||||||
|
const user = useUser()
|
||||||
|
const [reporting, setReporting] = useState(false)
|
||||||
|
if (!user) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
const userReported = contract.flaggedByUsernames?.includes(user.id)
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (!user || userReported) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
setReporting(true)
|
||||||
|
|
||||||
|
await updateContract(contract.id, {
|
||||||
|
flaggedByUsernames: buildArray(contract.flaggedByUsernames, user.id),
|
||||||
|
})
|
||||||
|
setReporting(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const flagClass = clsx(
|
||||||
|
'mx-2 flex flex-col items-center gap-1 w-6 h-6 rounded-md !bg-gray-100 px-2 py-1 hover:bg-gray-300',
|
||||||
|
userReported ? '!text-red-500' : '!text-gray-500'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
text={
|
||||||
|
userReported
|
||||||
|
? "You've reported this market as incorrectly resolved"
|
||||||
|
: 'Flag this market as incorrectly resolved '
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConfirmationButton
|
||||||
|
openModalBtn={{
|
||||||
|
label: '',
|
||||||
|
icon: <FlagIcon className="h-5 w-5" />,
|
||||||
|
className: clsx(flagClass, reporting && 'btn-disabled loading'),
|
||||||
|
}}
|
||||||
|
cancelBtn={{
|
||||||
|
label: 'Cancel',
|
||||||
|
className: 'border-none btn-sm btn-ghost self-center',
|
||||||
|
}}
|
||||||
|
submitBtn={{
|
||||||
|
label: 'Submit',
|
||||||
|
className: 'btn-secondary',
|
||||||
|
}}
|
||||||
|
onSubmitWithSuccess={onSubmit}
|
||||||
|
disabled={userReported}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Row className="items-center text-xl">
|
||||||
|
Flag this market as incorrectly resolved
|
||||||
|
</Row>
|
||||||
|
<Row className="text-sm text-gray-500">
|
||||||
|
Report that the market was not resolved according to its resolution
|
||||||
|
criteria. If a creator's markets get flagged too often, they'll be
|
||||||
|
marked as unreliable.
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</ConfirmationButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user