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
likedByUserIds?: string[]
likedByUserCount?: number
flaggedByUsernames?: string[]
openCommentBounties?: number
} & T

View File

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

View File

@ -69,6 +69,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true,
fractionResolvedCorrectly: 1,
}
await firestore.collection('users').doc(auth.uid).create(user)

View File

@ -135,6 +135,28 @@ export async function updateMetricsCore() {
lastPortfolio.investmentValue !== newPortfolio.investmentValue
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 {
user,
@ -142,6 +164,7 @@ export async function updateMetricsCore() {
newPortfolio,
newProfit,
didPortfolioChange,
newFractionResolvedCorrectly,
}
})
@ -163,6 +186,7 @@ export async function updateMetricsCore() {
newPortfolio,
newProfit,
didPortfolioChange,
newFractionResolvedCorrectly,
}) => {
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
return {
@ -172,6 +196,7 @@ export async function updateMetricsCore() {
creatorVolumeCached: newCreatorVolume,
profitCached: newProfit,
nextLoanCached,
fractionResolvedCorrectly: newFractionResolvedCorrectly,
},
},
@ -243,3 +268,5 @@ const topUserScores = (scores: { [userId: string]: 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
onOpenChanged?: (isOpen: boolean) => void
onSubmitWithSuccess?: () => Promise<boolean>
disabled?: boolean
}) {
const {
openModalBtn,
@ -35,6 +36,7 @@ export function ConfirmationButton(props: {
children,
onOpenChanged,
onSubmitWithSuccess,
disabled,
} = props
const [open, setOpen] = useState(false)
@ -72,9 +74,15 @@ export function ConfirmationButton(props: {
</Row>
</Col>
</Modal>
<Button
className={clsx(openModalBtn.className)}
onClick={() => updateOpen(true)}
className={openModalBtn.className}
onClick={() => {
if (disabled) {
return
}
updateOpen(true)
}}
disabled={openModalBtn.disabled}
color={openModalBtn.color}
size={openModalBtn.size}

View File

@ -218,17 +218,19 @@ export function BinaryResolutionOrChance(props: {
className={clsx('items-end', large ? 'text-4xl' : 'text-3xl', className)}
>
{resolution ? (
<>
<div
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
>
Resolved
<Row className="flex items-start">
<div>
<div
className={clsx('text-gray-500', large ? 'text-xl' : 'text-base')}
>
Resolved
</div>
<BinaryContractOutcomeLabel
contract={contract}
resolution={resolution}
/>
</div>
<BinaryContractOutcomeLabel
contract={contract}
resolution={resolution}
/>
</>
</Row>
) : (
<>
{probAfter && probChanged ? (

View File

@ -14,7 +14,7 @@ import { useState } from 'react'
import NewContractBadge from '../new-contract-badge'
import { MiniUserFollowButton } from '../follow-button'
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 { Button } from 'web/components/button'
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 { Tooltip } from 'web/components/tooltip'
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 { Subtitle } from '../subtitle'
import { useIsMobile } from 'web/hooks/use-is-mobile'
@ -149,6 +149,8 @@ export function MarketSubheader(props: {
const { creatorName, creatorUsername, creatorId, creatorAvatarUrl } = contract
const { resolvedDate } = contractMetrics(contract)
const user = useUser()
const correctResolutionPercentage =
useUserById(creatorId)?.fractionResolvedCorrectly
const isCreator = user?.id === creatorId
const isMobile = useIsMobile()
return (
@ -160,13 +162,14 @@ export function MarketSubheader(props: {
size={9}
className="mr-1.5"
/>
{!disabled && (
<div className="absolute mt-3 ml-[11px]">
<MiniUserFollowButton userId={creatorId} />
</div>
)}
<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 ? (
creatorName
) : (
@ -177,6 +180,12 @@ export function MarketSubheader(props: {
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 className="text-2xs text-greyscale-4 flex-wrap gap-2 sm:text-xs">
<CloseOrResolveTime
@ -487,3 +496,5 @@ function EditableCloseDate(props: {
</>
)
}
const BAD_CREATOR_THRESHOLD = 0.8

View File

@ -24,6 +24,7 @@ import {
BinaryContract,
} from 'common/contract'
import { ContractDetails } from './contract-details'
import { ContractReportResolution } from './contract-report-resolution'
const OverviewQuestion = (props: { text: string }) => (
<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} />
<Row className="justify-between gap-4">
<OverviewQuestion text={contract.question} />
<BinaryResolutionOrChance contract={contract} large />
<Row>
<BinaryResolutionOrChance
className="flex items-end"
contract={contract}
large
/>
{contract.isResolved && (
<ContractReportResolution contract={contract} />
)}
</Row>
</Row>
</Col>
<SizedContractChart
@ -144,7 +154,13 @@ const ChoiceOverview = (props: {
<ContractDetails contract={contract} />
<OverviewQuestion text={question} />
{resolution && (
<FreeResponseResolutionOrChance contract={contract} truncate="none" />
<Row>
<FreeResponseResolutionOrChance
contract={contract}
truncate="none"
/>
<ContractReportResolution contract={contract} />
</Row>
)}
</Col>
<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>
)
}