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:
FRC 2022-10-03 10:49:19 +01:00 committed by GitHub
parent 3fb43c16c4
commit 06571a3657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 17 deletions

View File

@ -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

View File

@ -33,6 +33,8 @@ export type User = {
allTime: number allTime: number
} }
fractionResolvedCorrectly: number
nextLoanCached: number nextLoanCached: number
followerCountCached: number followerCountCached: number

View File

@ -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)

View File

@ -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

View File

@ -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}

View File

@ -218,7 +218,8 @@ 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 <div
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')} className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
> >
@ -228,7 +229,8 @@ export function BinaryResolutionOrChance(props: {
contract={contract} contract={contract}
resolution={resolution} resolution={resolution}
/> />
</> </div>
</Row>
) : ( ) : (
<> <>
{probAfter && probChanged ? ( {probAfter && probChanged ? (

View File

@ -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

View File

@ -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

View 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>
)
}