Merge branch 'refactoring' into automated-market-resolution
# Conflicts: # firestore.rules
This commit is contained in:
commit
722a6e6d94
|
@ -7,23 +7,28 @@ service cloud.firestore {
|
||||||
|
|
||||||
function isAdmin() {
|
function isAdmin() {
|
||||||
return request.auth.uid == 'igi2zGXsfxYPgB0DJTXVJVmwCOr2' // Austin
|
return request.auth.uid == 'igi2zGXsfxYPgB0DJTXVJVmwCOr2' // Austin
|
||||||
|| request.auth.uid == '5LZ4LgYuySdL1huCWe7bti02ghx2' // James
|
|| request.auth.uid == '5LZ4LgYuySdL1huCWe7bti02ghx2' // James
|
||||||
|| request.auth.uid == 'tlmGNz9kjXc2EteizMORes4qvWl2' // Stephen
|
|| request.auth.uid == 'tlmGNz9kjXc2EteizMORes4qvWl2' // Stephen
|
||||||
|| request.auth.uid == 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // Manifold
|
|| request.auth.uid == 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // Manifold
|
||||||
}
|
}
|
||||||
|
|
||||||
match /users/{userId} {
|
match /users/{userId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if resource.data.id == request.auth.uid
|
allow update: if resource.data.id == request.auth.uid
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
|
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
|
||||||
|
}
|
||||||
|
|
||||||
|
match /users/{userId}/follows/{followUserId} {
|
||||||
|
allow read;
|
||||||
|
allow write: if request.auth.uid == userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId} {
|
match /private-users/{userId} {
|
||||||
allow read: if resource.data.id == request.auth.uid || isAdmin();
|
allow read: if resource.data.id == request.auth.uid || isAdmin();
|
||||||
allow update: if (resource.data.id == request.auth.uid || isAdmin())
|
allow update: if (resource.data.id == request.auth.uid || isAdmin())
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['apiKey']);
|
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
match /private-users/{userId}/views/{viewId} {
|
match /private-users/{userId}/views/{viewId} {
|
||||||
|
@ -45,8 +50,10 @@ service cloud.firestore {
|
||||||
match /contracts/{contractId} {
|
match /contracts/{contractId} {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['description', 'closeTime', 'tags', 'lowercaseTags', 'autoResolutionTime'])
|
.hasOnly(['tags', 'lowercaseTags']);
|
||||||
&& resource.data.id == request.auth.uid;
|
allow update: if request.resource.data.diff(resource.data).affectedKeys()
|
||||||
|
.hasOnly(['description', 'closeTime', 'autoResolveDate'])
|
||||||
|
&& resource.data.creatorId == request.auth.uid;
|
||||||
allow update: if isAdmin();
|
allow update: if isAdmin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,5 +96,12 @@ service cloud.firestore {
|
||||||
match /txns/{txnId} {
|
match /txns/{txnId} {
|
||||||
allow read;
|
allow read;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match /users/{userId}/notifications/{notificationId} {
|
||||||
|
allow read;
|
||||||
|
allow update: if resource.data.userId == request.auth.uid
|
||||||
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
|
.hasOnly(['isSeen', 'viewTime']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,8 @@ export const createNotification = async (
|
||||||
// TODO: Update for liquidity.
|
// TODO: Update for liquidity.
|
||||||
// TODO: Find tagged users.
|
// TODO: Find tagged users.
|
||||||
// TODO: Find replies to comments.
|
// TODO: Find replies to comments.
|
||||||
// TODO: Filter bets for only open bets
|
// TODO: Filter bets for only open bets.
|
||||||
|
// TODO: Notify users of their own closed but not resolved contracts.
|
||||||
if (
|
if (
|
||||||
sourceType === 'comment' ||
|
sourceType === 'comment' ||
|
||||||
sourceType === 'answer' ||
|
sourceType === 'answer' ||
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const sellBet = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||||
if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' }
|
if (!betSnap.exists) return { status: 'error', message: 'Invalid bet' }
|
||||||
const bet = betSnap.data() as Bet
|
const bet = betSnap.data() as Bet
|
||||||
|
|
||||||
|
if (userId !== bet.userId) return { status: 'error', message: 'Not authorized' }
|
||||||
if (bet.isSold) return { status: 'error', message: 'Bet already sold' }
|
if (bet.isSold) return { status: 'error', message: 'Bet already sold' }
|
||||||
|
|
||||||
const newBetDoc = firestore
|
const newBetDoc = firestore
|
||||||
|
|
|
@ -392,7 +392,7 @@ export function BetsSummary(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
|
<Row className={clsx('flex-wrap gap-4 sm:flex-nowrap sm:gap-6', className)}>
|
||||||
<Row className="gap-4 sm:gap-6">
|
<Row className="flex-wrap gap-4 sm:gap-6">
|
||||||
{!isCpmm && (
|
{!isCpmm && (
|
||||||
<Col>
|
<Col>
|
||||||
<div className="whitespace-nowrap text-sm text-gray-500">
|
<div className="whitespace-nowrap text-sm text-gray-500">
|
||||||
|
|
|
@ -68,8 +68,6 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
// Catch any errors from hovering on an invalid option
|
// Catch any errors from hovering on an invalid option
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = getColor(contract, previewProb)
|
|
||||||
|
|
||||||
async function placeQuickBet(direction: 'UP' | 'DOWN') {
|
async function placeQuickBet(direction: 'UP' | 'DOWN') {
|
||||||
const betPromise = async () => {
|
const betPromise = async () => {
|
||||||
const outcome = quickOutcome(contract, direction)
|
const outcome = quickOutcome(contract, direction)
|
||||||
|
@ -128,14 +126,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
<TriangleFillIcon
|
<TriangleFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
upHover ? `text-${color}` : 'text-gray-400'
|
upHover ? 'text-green-500' : 'text-gray-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TriangleFillIcon
|
<TriangleFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
upHover ? `text-${color}` : 'text-gray-200'
|
upHover ? 'text-green-500' : 'text-gray-200'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -155,14 +153,14 @@ export function QuickBet(props: { contract: Contract; user: User }) {
|
||||||
<TriangleDownFillIcon
|
<TriangleDownFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
downHover ? `text-${color}` : 'text-gray-400'
|
downHover ? 'text-red-500' : 'text-gray-400'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TriangleDownFillIcon
|
<TriangleDownFillIcon
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'mx-auto h-5 w-5',
|
'mx-auto h-5 w-5',
|
||||||
downHover ? `text-${color}` : 'text-gray-200'
|
downHover ? 'text-red-500' : 'text-gray-200'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -271,10 +269,15 @@ export function getColor(contract: Contract, previewProb?: number) {
|
||||||
'primary'
|
'primary'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contract.outcomeType === 'NUMERIC') {
|
if (contract.outcomeType === 'NUMERIC') {
|
||||||
return 'blue-400'
|
return 'blue-400'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contract.outcomeType === 'FREE_RESPONSE') {
|
||||||
|
return 'blue-400'
|
||||||
|
}
|
||||||
|
|
||||||
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
const marketClosed = (contract.closeTime || Infinity) < Date.now()
|
||||||
const prob = previewProb ?? getProb(contract)
|
const prob = previewProb ?? getProb(contract)
|
||||||
return marketClosed ? 'gray-400' : prob >= 0.5 ? 'primary' : 'red-400'
|
return marketClosed ? 'gray-400' : prob >= 0.5 ? 'primary' : 'red-400'
|
||||||
|
|
|
@ -8,13 +8,15 @@ import Link from 'next/link'
|
||||||
import { fromNow } from 'web/lib/util/time'
|
import { fromNow } from 'web/lib/util/time'
|
||||||
import { ToastClipboard } from 'web/components/toast-clipboard'
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
import { LinkIcon } from '@heroicons/react/outline'
|
import { LinkIcon } from '@heroicons/react/outline'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
export function CopyLinkDateTimeComponent(props: {
|
export function CopyLinkDateTimeComponent(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
createdTime: number
|
createdTime: number
|
||||||
elementId: string
|
elementId: string
|
||||||
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { contract, elementId, createdTime } = props
|
const { contract, elementId, createdTime, className } = props
|
||||||
const [showToast, setShowToast] = useState(false)
|
const [showToast, setShowToast] = useState(false)
|
||||||
|
|
||||||
function copyLinkToComment(
|
function copyLinkToComment(
|
||||||
|
@ -30,7 +32,7 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
setTimeout(() => setShowToast(false), 2000)
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={clsx('inline', className)}>
|
||||||
<DateTimeTooltip time={createdTime}>
|
<DateTimeTooltip time={createdTime}>
|
||||||
<Link
|
<Link
|
||||||
href={`/${contract.creatorUsername}/${contract.slug}#${elementId}`}
|
href={`/${contract.creatorUsername}/${contract.slug}#${elementId}`}
|
||||||
|
@ -53,6 +55,6 @@ export function CopyLinkDateTimeComponent(props: {
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</DateTimeTooltip>
|
</DateTimeTooltip>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@ export function FeedComment(props: {
|
||||||
avatarUrl={userAvatarUrl}
|
avatarUrl={userAvatarUrl}
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="mt-0.5 pl-0.5 text-sm text-gray-500">
|
<div className="mt-0.5 pl-0.5 text-sm text-gray-500">
|
||||||
<UserLink
|
<UserLink
|
||||||
className="text-gray-500"
|
className="text-gray-500"
|
||||||
username={userUsername}
|
username={userUsername}
|
||||||
|
@ -204,7 +204,7 @@ export function FeedComment(props: {
|
||||||
createdTime={createdTime}
|
createdTime={createdTime}
|
||||||
elementId={comment.id}
|
elementId={comment.id}
|
||||||
/>
|
/>
|
||||||
</p>
|
</div>
|
||||||
<TruncatedComment
|
<TruncatedComment
|
||||||
comment={text}
|
comment={text}
|
||||||
moreHref={contractPath(contract)}
|
moreHref={contractPath(contract)}
|
||||||
|
|
|
@ -174,21 +174,16 @@ export function NewContract(props: { question: string; tag?: string }) {
|
||||||
<ChoicesToggleGroup
|
<ChoicesToggleGroup
|
||||||
currentChoice={outcomeType}
|
currentChoice={outcomeType}
|
||||||
setChoice={(choice) => {
|
setChoice={(choice) => {
|
||||||
if (choice === 'NUMERIC')
|
if (choice === 'FREE_RESPONSE')
|
||||||
setMarketInfoText(
|
|
||||||
'Numeric markets are still experimental and subject to major revisions.'
|
|
||||||
)
|
|
||||||
else if (choice === 'FREE_RESPONSE')
|
|
||||||
setMarketInfoText(
|
setMarketInfoText(
|
||||||
'Users can submit their own answers to this market.'
|
'Users can submit their own answers to this market.'
|
||||||
)
|
)
|
||||||
else setMarketInfoText('')
|
else setMarketInfoText('')
|
||||||
setOutcomeType(choice as outcomeType)
|
setOutcomeType(choice as 'BINARY' | 'FREE_RESPONSE')
|
||||||
}}
|
}}
|
||||||
choicesMap={{
|
choicesMap={{
|
||||||
'Yes / No': 'BINARY',
|
'Yes / No': 'BINARY',
|
||||||
'Free response': 'FREE_RESPONSE',
|
'Free response': 'FREE_RESPONSE',
|
||||||
Numeric: 'NUMERIC',
|
|
||||||
}}
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
className={'col-span-4'}
|
className={'col-span-4'}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Notification } from 'common/notification'
|
import {
|
||||||
|
Notification,
|
||||||
|
notification_reason_types,
|
||||||
|
notification_source_types,
|
||||||
|
} from 'common/notification'
|
||||||
import { listenForNotifications } from 'web/lib/firebase/notifications'
|
import { listenForNotifications } from 'web/lib/firebase/notifications'
|
||||||
import { Avatar } from 'web/components/avatar'
|
import { Avatar } from 'web/components/avatar'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
|
@ -15,9 +19,9 @@ import { Comment } from 'web/lib/firebase/comments'
|
||||||
import { getValue } from 'web/lib/firebase/utils'
|
import { getValue } from 'web/lib/firebase/utils'
|
||||||
import Custom404 from 'web/pages/404'
|
import Custom404 from 'web/pages/404'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
import { Linkify } from 'web/components/linkify'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { useContract } from 'web/hooks/use-contract'
|
import { useContract } from 'web/hooks/use-contract'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -37,8 +41,8 @@ export default function Notifications() {
|
||||||
// TODO: use infinite scroll
|
// TODO: use infinite scroll
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className={'p-4'}>
|
<div className={'p-2 sm:p-4'}>
|
||||||
<Title text={'Notifications'} />
|
<Title text={'Notifications'} className={'hidden md:block'} />
|
||||||
<Tabs
|
<Tabs
|
||||||
className={'pb-2 pt-1 '}
|
className={'pb-2 pt-1 '}
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
|
@ -79,6 +83,7 @@ function Notification(props: {
|
||||||
sourceUserName,
|
sourceUserName,
|
||||||
sourceUserAvatarUrl,
|
sourceUserAvatarUrl,
|
||||||
reasonText,
|
reasonText,
|
||||||
|
reason,
|
||||||
sourceUserUsername,
|
sourceUserUsername,
|
||||||
createdTime,
|
createdTime,
|
||||||
} = notification
|
} = notification
|
||||||
|
@ -140,7 +145,7 @@ function Notification(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={' bg-white px-4 pt-6'}>
|
<div className={'bg-white px-1 pt-6 text-sm sm:px-4'}>
|
||||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={sourceUserAvatarUrl}
|
avatarUrl={sourceUserAvatarUrl}
|
||||||
|
@ -148,35 +153,47 @@ function Notification(props: {
|
||||||
className={'mr-2'}
|
className={'mr-2'}
|
||||||
username={sourceUserName}
|
username={sourceUserName}
|
||||||
/>
|
/>
|
||||||
<div className={'flex-1'}>
|
<div className={'flex-1 overflow-hidden sm:flex'}>
|
||||||
<UserLink
|
<div
|
||||||
name={sourceUserName || ''}
|
className={
|
||||||
username={sourceUserUsername || ''}
|
'flex max-w-sm shrink overflow-hidden text-ellipsis sm:max-w-md'
|
||||||
className={'mr-0 flex-shrink-0'}
|
}
|
||||||
/>
|
>
|
||||||
<a href={getSourceUrl(sourceId)} className={'flex-1 pl-1'}>
|
<UserLink
|
||||||
{reasonText}
|
name={sourceUserName || ''}
|
||||||
{contract && sourceId && (
|
username={sourceUserUsername || ''}
|
||||||
<div className={'inline'}>
|
className={'mr-0 flex-shrink-0'}
|
||||||
<CopyLinkDateTimeComponent
|
/>
|
||||||
contract={contract}
|
<a
|
||||||
createdTime={createdTime}
|
href={getSourceUrl(sourceId)}
|
||||||
elementId={getSourceIdForLinkComponent(sourceId)}
|
className={'inline-flex overflow-hidden text-ellipsis pl-1'}
|
||||||
/>
|
>
|
||||||
</div>
|
{sourceType && reason ? (
|
||||||
)}
|
<div className={'inline truncate'}>
|
||||||
</a>
|
{getReasonTextFromReason(sourceType, reason, contract)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
reasonText
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{contract && sourceId && (
|
||||||
|
<CopyLinkDateTimeComponent
|
||||||
|
contract={contract}
|
||||||
|
createdTime={createdTime}
|
||||||
|
elementId={getSourceIdForLinkComponent(sourceId)}
|
||||||
|
className={'-mx-1 inline-flex sm:inline-block'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
<a href={getSourceUrl(sourceId)}>
|
<a href={getSourceUrl(sourceId)}>
|
||||||
<div className={'ml-4 mt-1'}>
|
<div className={'mt-1 md:text-base'}>
|
||||||
{' '}
|
{' '}
|
||||||
{contract && subText === contract.question ? (
|
{contract && subText === contract.question ? (
|
||||||
<div className={'text-md text-indigo-700 hover:underline'}>
|
<div className={'text-indigo-700 hover:underline'}>{subText}</div>
|
||||||
{subText}
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<Linkify text={subText} />
|
<div className={'line-clamp-4 whitespace-pre-line'}>{subText}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -185,3 +202,20 @@ function Notification(props: {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReasonTextFromReason(
|
||||||
|
source: notification_source_types,
|
||||||
|
reason: notification_reason_types,
|
||||||
|
contract: Contract | undefined
|
||||||
|
) {
|
||||||
|
switch (source) {
|
||||||
|
case 'comment':
|
||||||
|
return `commented on ${contract?.question}`
|
||||||
|
case 'contract':
|
||||||
|
return `${reason} ${contract?.question}`
|
||||||
|
case 'answer':
|
||||||
|
return `answered ${contract?.question}`
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user