Categories (#132)

* basic market categories

* use tags to store market category

* display category in market

* display full category

* category selector component on feed

* Move feed data fetching to new file

* Decrease batch size for updating feed to prevent out-of-memory error

* Compute and update category feeds!

* Show feeds based on category tabs

* Add react-query package!

* Use react query to cache contracts

* Remove 'other' category

* Add back personal / friends to feed categories

* Show scrollbar temporarily for categories

* Remove 5 categories, change geopolitics to world

* finance => economics

* Show categories on two lines on larger screens

Co-authored-by: James Grugett <jahooma@gmail.com>
This commit is contained in:
mantikoros 2022-05-12 10:07:10 -05:00 committed by GitHub
parent 403156ed1a
commit 9a4e5763f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 476 additions and 144 deletions

25
common/categories.ts Normal file
View File

@ -0,0 +1,25 @@
export const CATEGORIES = {
politics: 'Politics',
technology: 'Technology',
sports: 'Sports',
gaming: 'Gaming / Esports',
manifold: 'Manifold Markets',
science: 'Science',
world: 'World',
fun: 'Fun stuff',
personal: 'Personal',
economics: 'Economics',
crypto: 'Crypto',
health: 'Health',
// entertainment: 'Entertainment',
// society: 'Society',
// friends: 'Friends / Community',
// business: 'Business',
// charity: 'Charities / Non-profits',
} as { [category: string]: string }
export const TO_CATEGORY = Object.fromEntries(
Object.entries(CATEGORIES).map(([k, v]) => [v, k])
)
export const CATEGORY_LIST = Object.keys(CATEGORIES)

9
common/feed.ts Normal file
View File

@ -0,0 +1,9 @@
import { Bet } from './bet'
import { Comment } from './comment'
import { Contract } from './contract'
export type feed = {
contract: Contract
recentBets: Bet[]
recentComments: Comment[]
}[]

View File

@ -17,6 +17,8 @@ export type User = {
totalDeposits: number totalDeposits: number
totalPnLCached: number totalPnLCached: number
creatorVolumeCached: number creatorVolumeCached: number
followedCategories?: string[]
} }
export const STARTING_BALANCE = 1000 export const STARTING_BALANCE = 1000

View File

@ -16,7 +16,7 @@ service cloud.firestore {
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']); .hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
} }
match /private-users/{userId} { match /private-users/{userId} {
@ -35,7 +35,7 @@ service cloud.firestore {
allow create: if userId == request.auth.uid; allow create: if userId == request.auth.uid;
} }
match /private-users/{userId}/cache/feed { match /private-users/{userId}/cache/{docId} {
allow read: if userId == request.auth.uid || isAdmin(); allow read: if userId == request.auth.uid || isAdmin();
} }

View File

@ -19,11 +19,13 @@
}, },
"main": "lib/functions/src/index.js", "main": "lib/functions/src/index.js",
"dependencies": { "dependencies": {
"@react-query-firebase/firestore": "0.4.2",
"fetch": "1.1.0", "fetch": "1.1.0",
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.16.0", "firebase-functions": "3.16.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"react-query": "3.39.0",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"stripe": "8.194.0" "stripe": "8.194.0"
}, },

View File

@ -7,8 +7,8 @@ import { getNewMultiBetInfo } from 'common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from 'common/answer' import { Answer, MAX_ANSWER_LENGTH } from 'common/answer'
import { getContract, getValues } from './utils' import { getContract, getValues } from './utils'
import { sendNewAnswerEmail } from './emails' import { sendNewAnswerEmail } from './emails'
import { Bet } from 'common/bet' import { Bet } from '../../common/bet'
import { hasUserHitManaLimit } from 'common/calculate' import { hasUserHitManaLimit } from '../../common/calculate'
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall( export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
async ( async (

View File

@ -1,6 +1,7 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import * as _ from 'lodash' import * as _ from 'lodash'
import { chargeUser, getUser } from './utils' import { chargeUser, getUser } from './utils'
import { import {
Binary, Binary,

View File

@ -0,0 +1,71 @@
import * as admin from 'firebase-admin'
import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract'
import { DAY_MS } from '../../common/util/time'
import { getValues } from './utils'
const firestore = admin.firestore()
export async function getFeedContracts() {
// Get contracts bet on or created in last week.
const [activeContracts, inactiveContracts] = await Promise.all([
getValues<Contract>(
firestore
.collection('contracts')
.where('isResolved', '==', false)
.where('volume7Days', '>', 0)
),
getValues<Contract>(
firestore
.collection('contracts')
.where('isResolved', '==', false)
.where('createdTime', '>', Date.now() - DAY_MS * 7)
.where('volume7Days', '==', 0)
),
])
const combined = [...activeContracts, ...inactiveContracts]
// Remove closed contracts.
return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
}
export async function getTaggedContracts(tag: string) {
const taggedContracts = await getValues<Contract>(
firestore
.collection('contracts')
.where('isResolved', '==', false)
.where('lowercaseTags', 'array-contains', tag.toLowerCase())
)
// Remove closed contracts.
return taggedContracts.filter((c) => (c.closeTime ?? Infinity) > Date.now())
}
export async function getRecentBetsAndComments(contract: Contract) {
const contractDoc = firestore.collection('contracts').doc(contract.id)
const [recentBets, recentComments] = await Promise.all([
getValues<Bet>(
contractDoc
.collection('bets')
.where('createdTime', '>', Date.now() - DAY_MS)
.orderBy('createdTime', 'desc')
.limit(1)
),
getValues<Comment>(
contractDoc
.collection('comments')
.where('createdTime', '>', Date.now() - 3 * DAY_MS)
.orderBy('createdTime', 'desc')
.limit(3)
),
])
return {
contract,
recentBets,
recentComments,
}
}

View File

@ -9,7 +9,9 @@ import { User } from 'common/user'
import { batchedWaitAll } from 'common/util/promise' import { batchedWaitAll } from 'common/util/promise'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { updateWordScores } from '../update-recommendations' import { updateWordScores } from '../update-recommendations'
import { getFeedContracts, doUserFeedUpdate } from '../update-feed' import { computeFeed } from '../update-feed'
import { getFeedContracts, getTaggedContracts } from '../get-feed-data'
import { CATEGORY_LIST } from '../../../common/categories'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -19,8 +21,7 @@ async function updateFeed() {
const contracts = await getValues<Contract>(firestore.collection('contracts')) const contracts = await getValues<Contract>(firestore.collection('contracts'))
const feedContracts = await getFeedContracts() const feedContracts = await getFeedContracts()
const users = await getValues<User>( const users = await getValues<User>(
firestore.collection('users') firestore.collection('users').where('username', '==', 'JamesGrugett')
// .where('username', '==', 'JamesGrugett')
) )
await batchedWaitAll( await batchedWaitAll(
@ -28,7 +29,22 @@ async function updateFeed() {
console.log('Updating recs for', user.username) console.log('Updating recs for', user.username)
await updateWordScores(user, contracts) await updateWordScores(user, contracts)
console.log('Updating feed for', user.username) console.log('Updating feed for', user.username)
await doUserFeedUpdate(user, feedContracts) await computeFeed(user, feedContracts)
})
)
console.log('Updating feed categories!')
await batchedWaitAll(
users.map((user) => async () => {
for (const category of CATEGORY_LIST) {
const contracts = await getTaggedContracts(category)
const feed = await computeFeed(user, contracts)
await firestore
.collection(`private-users/${user.id}/cache`)
.doc(`feed-${category}`)
.set({ feed })
}
}) })
) )
} }

View File

@ -10,15 +10,19 @@ import {
getProbability, getProbability,
getOutcomeProbability, getOutcomeProbability,
getTopAnswer, getTopAnswer,
} from 'common/calculate' } from '../../common/calculate'
import { Bet } from 'common/bet' import { User } from '../../common/user'
import { Comment } from 'common/comment'
import { User } from 'common/user'
import { import {
getContractScore, getContractScore,
MAX_FEED_CONTRACTS, MAX_FEED_CONTRACTS,
} from 'common/recommended-contracts' } from 'common/recommended-contracts'
import { callCloudFunction } from './call-cloud-function' import { callCloudFunction } from './call-cloud-function'
import {
getFeedContracts,
getRecentBetsAndComments,
getTaggedContracts,
} from './get-feed-data'
import { CATEGORY_LIST } from '../../common/categories'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -28,16 +32,28 @@ export const updateFeed = functions.pubsub
const users = await getValues<User>(firestore.collection('users')) const users = await getValues<User>(firestore.collection('users'))
const batchSize = 100 const batchSize = 100
const userBatches: User[][] = [] let userBatches: User[][] = []
for (let i = 0; i < users.length; i += batchSize) { for (let i = 0; i < users.length; i += batchSize) {
userBatches.push(users.slice(i, i + batchSize)) userBatches.push(users.slice(i, i + batchSize))
} }
console.log('updating feed batch')
await Promise.all( await Promise.all(
userBatches.map(async (users) => userBatches.map((users) =>
callCloudFunction('updateFeedBatch', { users }) callCloudFunction('updateFeedBatch', { users })
) )
) )
console.log('updating category feed')
await Promise.all(
CATEGORY_LIST.map((category) =>
callCloudFunction('updateCategoryFeed', {
category,
})
)
)
}) })
export const updateFeedBatch = functions.https.onCall( export const updateFeedBatch = functions.https.onCall(
@ -45,40 +61,56 @@ export const updateFeedBatch = functions.https.onCall(
const { users } = data const { users } = data
const contracts = await getFeedContracts() const contracts = await getFeedContracts()
await Promise.all(users.map((user) => doUserFeedUpdate(user, contracts))) await Promise.all(
users.map(async (user) => {
const feed = await computeFeed(user, contracts)
await getUserCacheCollection(user).doc('feed').set({ feed })
})
)
}
)
export const updateCategoryFeed = functions.https.onCall(
async (data: { category: string }) => {
const { category } = data
const users = await getValues<User>(firestore.collection('users'))
const batchSize = 100
const userBatches: User[][] = []
for (let i = 0; i < users.length; i += batchSize) {
userBatches.push(users.slice(i, i + batchSize))
}
await Promise.all(
userBatches.map(async (users) => {
await callCloudFunction('updateCategoryFeedBatch', {
users,
category,
})
})
)
} }
) )
export async function getFeedContracts() { export const updateCategoryFeedBatch = functions.https.onCall(
// Get contracts bet on or created in last week. async (data: { users: User[]; category: string }) => {
const contracts = await Promise.all([ const { users, category } = data
getValues<Contract>( const contracts = await getTaggedContracts(category)
firestore
.collection('contracts')
.where('isResolved', '==', false)
.where('volume7Days', '>', 0)
),
getValues<Contract>( await Promise.all(
firestore users.map(async (user) => {
.collection('contracts') const feed = await computeFeed(user, contracts)
.where('isResolved', '==', false) await getUserCacheCollection(user).doc(`feed-${category}`).set({ feed })
.where('createdTime', '>', Date.now() - DAY_MS * 7)
.where('volume7Days', '==', 0)
),
]).then(([activeContracts, inactiveContracts]) => {
const combined = [...activeContracts, ...inactiveContracts]
// Remove closed contracts.
return combined.filter((c) => (c.closeTime ?? Infinity) > Date.now())
}) })
return contracts
}
export const doUserFeedUpdate = async (user: User, contracts: Contract[]) => {
const userCacheCollection = firestore.collection(
`private-users/${user.id}/cache`
) )
}
)
const getUserCacheCollection = (user: User) =>
firestore.collection(`private-users/${user.id}/cache`)
export const computeFeed = async (user: User, contracts: Contract[]) => {
const userCacheCollection = getUserCacheCollection(user)
const [wordScores, lastViewedTime] = await Promise.all([ const [wordScores, lastViewedTime] = await Promise.all([
getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')), getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')),
getValue<{ [contractId: string]: number }>( getValue<{ [contractId: string]: number }>(
@ -109,8 +141,7 @@ export const doUserFeedUpdate = async (user: User, contracts: Contract[]) => {
const feed = await Promise.all( const feed = await Promise.all(
feedContracts.map((contract) => getRecentBetsAndComments(contract)) feedContracts.map((contract) => getRecentBetsAndComments(contract))
) )
return feed
await userCacheCollection.doc('feed').set({ feed })
} }
function scoreContract( function scoreContract(
@ -180,31 +211,3 @@ function getLastViewedScore(viewTime: number | undefined) {
const frac = logInterpolation(0.5, 14, daysAgo) const frac = logInterpolation(0.5, 14, daysAgo)
return 0.75 + 0.25 * frac return 0.75 + 0.25 * frac
} }
async function getRecentBetsAndComments(contract: Contract) {
const contractDoc = firestore.collection('contracts').doc(contract.id)
const [recentBets, recentComments] = await Promise.all([
getValues<Bet>(
contractDoc
.collection('bets')
.where('createdTime', '>', Date.now() - DAY_MS)
.orderBy('createdTime', 'desc')
.limit(1)
),
getValues<Comment>(
contractDoc
.collection('comments')
.where('createdTime', '>', Date.now() - 3 * DAY_MS)
.orderBy('createdTime', 'desc')
.limit(3)
),
])
return {
contract,
recentBets,
recentComments,
}
}

View File

@ -2,6 +2,7 @@ import clsx from 'clsx'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useState } from 'react' import { useState } from 'react'
import Textarea from 'react-expanding-textarea' import Textarea from 'react-expanding-textarea'
import { CATEGORY_LIST } from '../../../common/categories'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { parseTags } from 'common/util/parse' import { parseTags } from 'common/util/parse'
@ -9,6 +10,7 @@ import { useAdmin } from 'web/hooks/use-admin'
import { updateContract } from 'web/lib/firebase/contracts' import { updateContract } from 'web/lib/firebase/contracts'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Linkify } from '../linkify' import { Linkify } from '../linkify'
import { TagsList } from '../tags-list'
export function ContractDescription(props: { export function ContractDescription(props: {
contract: Contract contract: Contract
@ -26,6 +28,7 @@ export function ContractDescription(props: {
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}` `${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
) )
const lowercaseTags = tags.map((tag) => tag.toLowerCase()) const lowercaseTags = tags.map((tag) => tag.toLowerCase())
await updateContract(contract.id, { await updateContract(contract.id, {
description: newDescription, description: newDescription,
tags, tags,
@ -35,6 +38,9 @@ export function ContractDescription(props: {
if (!isCreator && !contract.description.trim()) return null if (!isCreator && !contract.description.trim()) return null
const { tags } = contract
const category = tags.find((tag) => CATEGORY_LIST.includes(tag.toLowerCase()))
return ( return (
<div <div
className={clsx( className={clsx(
@ -43,7 +49,15 @@ export function ContractDescription(props: {
)} )}
> >
<Linkify text={contract.description} /> <Linkify text={contract.description} />
{category && (
<div className="mt-4">
<TagsList tags={[category]} label="Category" />
</div>
)}
<br /> <br />
{isCreator && ( {isCreator && (
<EditContract <EditContract
// Note: Because descriptionTimestamp is called once, later edits use // Note: Because descriptionTimestamp is called once, later edits use

View File

@ -0,0 +1,75 @@
import clsx from 'clsx'
import _ from 'lodash'
import { User } from '../../../common/user'
import { Row } from '../layout/row'
import { CATEGORIES, CATEGORY_LIST } from '../../../common/categories'
import { updateUser } from '../../lib/firebase/users'
export function CategorySelector(props: {
user: User | null | undefined
className?: string
}) {
const { className, user } = props
const followedCategories = user?.followedCategories ?? []
return (
<Row
className={clsx(
'mr-2 items-center space-x-2 space-y-2 overflow-x-scroll scroll-smooth pt-4 pb-4 sm:flex-wrap',
className
)}
>
<div />
<CategoryButton
key={'all' + followedCategories.length}
category="All"
isFollowed={followedCategories.length === 0}
toggle={async () => {
if (!user?.id) return
await updateUser(user.id, {
followedCategories: [],
})
}}
/>
{CATEGORY_LIST.map((cat) => (
<CategoryButton
key={cat + followedCategories.length}
category={CATEGORIES[cat].split(' ')[0]}
isFollowed={followedCategories.includes(cat)}
toggle={async () => {
if (!user?.id) return
await updateUser(user.id, {
followedCategories: [cat],
})
}}
/>
))}
</Row>
)
}
function CategoryButton(props: {
category: string
isFollowed: boolean
toggle: () => void
}) {
const { toggle, category, isFollowed } = props
return (
<div
className={clsx(
'rounded-full border-2 px-4 py-1 shadow-md',
'cursor-pointer',
isFollowed ? 'border-gray-300 bg-gray-300' : 'bg-white'
)}
onClick={toggle}
>
<span className="text-sm text-gray-500">{category}</span>
</div>
)
}

View File

@ -10,6 +10,7 @@ import {
} from './activity-items' } from './activity-items'
import { FeedItems } from './feed-items' import { FeedItems } from './feed-items'
import { User } from 'common/user' import { User } from 'common/user'
import { useContract } from 'web/hooks/use-contract'
export function ContractActivity(props: { export function ContractActivity(props: {
contract: Contract contract: Contract
@ -27,8 +28,9 @@ export function ContractActivity(props: {
className?: string className?: string
betRowClassName?: string betRowClassName?: string
}) { }) {
const { contract, user, mode, contractPath, className, betRowClassName } = const { user, mode, contractPath, className, betRowClassName } = props
props
const contract = useContract(props.contract.id) ?? props.contract
const updatedComments = const updatedComments =
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks

View File

@ -1,4 +1,5 @@
import clsx from 'clsx' import clsx from 'clsx'
import { CATEGORIES } from '../../common/categories'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
@ -6,6 +7,9 @@ import { SiteLink } from './site-link'
function Hashtag(props: { tag: string; noLink?: boolean }) { function Hashtag(props: { tag: string; noLink?: boolean }) {
const { tag, noLink } = props const { tag, noLink } = props
const category = CATEGORIES[tag.replace('#', '').toLowerCase()]
console.log(tag, category)
const body = ( const body = (
<div <div
className={clsx( className={clsx(
@ -13,7 +17,7 @@ function Hashtag(props: { tag: string; noLink?: boolean }) {
!noLink && 'cursor-pointer' !noLink && 'cursor-pointer'
)} )}
> >
<span className="text-sm text-gray-600">{tag}</span> <span className="text-sm text-gray-600">{category ?? tag}</span>
</div> </div>
) )
@ -30,11 +34,12 @@ export function TagsList(props: {
className?: string className?: string
noLink?: boolean noLink?: boolean
noLabel?: boolean noLabel?: boolean
label?: string
}) { }) {
const { tags, className, noLink, noLabel } = props const { tags, className, noLink, noLabel, label } = props
return ( return (
<Row className={clsx('flex-wrap items-center gap-2', className)}> <Row className={clsx('flex-wrap items-center gap-2', className)}>
{!noLabel && <div className="mr-1 text-gray-500">Tags</div>} {!noLabel && <div className="mr-1 text-gray-500">{label || 'Tags'}</div>}
{tags.map((tag) => ( {tags.map((tag) => (
<Hashtag <Hashtag
key={tag} key={tag}

View File

@ -1,26 +1,18 @@
import _ from 'lodash' import _, { Dictionary } from 'lodash'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { Bet } from 'common/bet' import type { feed } from 'common/feed'
import { Comment } from 'common/comment'
import { Contract } from 'common/contract'
import { useTimeSinceFirstRender } from './use-time-since-first-render' import { useTimeSinceFirstRender } from './use-time-since-first-render'
import { trackLatency } from 'web/lib/firebase/tracking' import { trackLatency } from 'web/lib/firebase/tracking'
import { User } from 'common/user' import { User } from 'common/user'
import { getUserFeed } from 'web/lib/firebase/users' import { getCategoryFeeds, getUserFeed } from 'web/lib/firebase/users'
import { useUpdatedContracts } from './use-contracts'
import { import {
getRecentBetsAndComments, getRecentBetsAndComments,
getTopWeeklyContracts, getTopWeeklyContracts,
} from 'web/lib/firebase/contracts' } from 'web/lib/firebase/contracts'
type feed = {
contract: Contract
recentBets: Bet[]
recentComments: Comment[]
}[]
export const useAlgoFeed = (user: User | null | undefined) => { export const useAlgoFeed = (user: User | null | undefined) => {
const [feed, setFeed] = useState<feed>() const [feed, setFeed] = useState<feed>()
const [categoryFeeds, setCategoryFeeds] = useState<Dictionary<feed>>()
const getTime = useTimeSinceFirstRender() const getTime = useTimeSinceFirstRender()
@ -34,21 +26,21 @@ export const useAlgoFeed = (user: User | null | undefined) => {
trackLatency('feed', getTime()) trackLatency('feed', getTime())
console.log('feed load time', getTime()) console.log('feed load time', getTime())
}) })
getCategoryFeeds(user.id).then((feeds) => {
setCategoryFeeds(feeds)
console.log('category feeds load time', getTime())
})
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.id]) }, [user?.id])
return useUpdateFeed(feed) const followedCategory = user?.followedCategories?.[0] ?? 'all'
}
const useUpdateFeed = (feed: feed | undefined) => { const followedFeed =
const contracts = useUpdatedContracts(feed?.map((item) => item.contract)) followedCategory === 'all' ? feed : categoryFeeds?.[followedCategory]
return feed && contracts return followedFeed
? feed.map(({ contract, ...other }, i) => ({
...other,
contract: contracts[i],
}))
: undefined
} }
const getDefaultFeed = async () => { const getDefaultFeed = async () => {

View File

@ -1,17 +1,21 @@
import { useEffect, useState } from 'react' import { useEffect } from 'react'
import { Contract, listenForContract } from 'web/lib/firebase/contracts' import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
import {
Contract,
contractDocRef,
listenForContract,
} from 'web/lib/firebase/contracts'
import { useStateCheckEquality } from './use-state-check-equality' import { useStateCheckEquality } from './use-state-check-equality'
import { DocumentData } from 'firebase/firestore'
export const useContract = (contractId: string) => { export const useContract = (contractId: string) => {
const [contract, setContract] = useState<Contract | null | 'loading'>( const result = useFirestoreDocumentData<DocumentData, Contract>(
'loading' ['contracts', contractId],
contractDocRef(contractId),
{ subscribe: true, includeMetadataChanges: true }
) )
useEffect(() => { return result.isLoading ? undefined : result.data
if (contractId) return listenForContract(contractId, setContract)
}, [contractId])
return contract
} }
export const useContractWithPreload = (initial: Contract | null) => { export const useContractWithPreload = (initial: Contract | null) => {

View File

@ -79,6 +79,8 @@ export function tradingAllowed(contract: Contract) {
const db = getFirestore(app) const db = getFirestore(app)
export const contractCollection = collection(db, 'contracts') export const contractCollection = collection(db, 'contracts')
export const contractDocRef = (contractId: string) =>
doc(db, 'contracts', contractId)
// Push contract to Firestore // Push contract to Firestore
export async function setContract(contract: Contract) { export async function setContract(contract: Contract) {

View File

@ -25,9 +25,8 @@ import { PrivateUser, User } from 'common/user'
import { createUser } from './api-call' import { createUser } from './api-call'
import { getValue, getValues, listenForValue, listenForValues } from './utils' import { getValue, getValues, listenForValue, listenForValues } from './utils'
import { DAY_MS } from 'common/util/time' import { DAY_MS } from 'common/util/time'
import { Contract } from './contracts' import { feed } from 'common/feed'
import { Bet } from './bets' import { CATEGORY_LIST } from 'common/categories'
import { Comment } from './comments'
export type { User } export type { User }
@ -216,11 +215,18 @@ export async function getDailyNewUsers(
export async function getUserFeed(userId: string) { export async function getUserFeed(userId: string) {
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed') const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
const userFeed = await getValue<{ const userFeed = await getValue<{
feed: { feed: feed
contract: Contract
recentBets: Bet[]
recentComments: Comment[]
}[]
}>(feedDoc) }>(feedDoc)
return userFeed?.feed ?? [] return userFeed?.feed ?? []
} }
export async function getCategoryFeeds(userId: string) {
const cacheCollection = collection(db, 'private-users', userId, 'cache')
const feedData = await Promise.all(
CATEGORY_LIST.map((category) =>
getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`))
)
)
const feeds = feedData.map((data) => data?.feed ?? [])
return _.fromPairs(_.zip(CATEGORY_LIST, feeds) as [string, feed][])
}

View File

@ -4,6 +4,7 @@ import { useEffect } from 'react'
import Head from 'next/head' import Head from 'next/head'
import Script from 'next/script' import Script from 'next/script'
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll' import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
import { QueryClient, QueryClientProvider } from 'react-query'
function firstLine(msg: string) { function firstLine(msg: string) {
return msg.replace(/\r?\n.*/s, '') return msg.replace(/\r?\n.*/s, '')
@ -76,9 +77,14 @@ function MyApp({ Component, pageProps }: AppProps) {
content="width=device-width, initial-scale=1, maximum-scale=1" content="width=device-width, initial-scale=1, maximum-scale=1"
/> />
</Head> </Head>
<QueryClientProvider client={queryClient}>
<Component {...pageProps} /> <Component {...pageProps} />
</QueryClientProvider>
</> </>
) )
} }
const queryClient = new QueryClient()
export default MyApp export default MyApp

View File

@ -19,6 +19,8 @@ import { Row } from 'web/components/layout/row'
import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract' import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today' import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
import { removeUndefinedProps } from '../../common/util/object'
import { CATEGORIES, CATEGORY_LIST, TO_CATEGORY } from 'common/categories'
export default function Create() { export default function Create() {
const [question, setQuestion] = useState('') const [question, setQuestion] = useState('')
@ -67,8 +69,10 @@ export function NewContract(props: { question: string; tag?: string }) {
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY') const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
const [initialProb, setInitialProb] = useState(50) const [initialProb, setInitialProb] = useState(50)
const [description, setDescription] = useState('') const [description, setDescription] = useState('')
const [tagText, setTagText] = useState<string>(tag ?? '')
const tags = parseWordsAsTags(tagText) const [category, setCategory] = useState<string>('')
// const [tagText, setTagText] = useState<string>(tag ?? '')
// const tags = parseWordsAsTags(tagText)
const [ante, setAnte] = useState(FIXED_ANTE) const [ante, setAnte] = useState(FIXED_ANTE)
@ -110,15 +114,17 @@ export function NewContract(props: { question: string; tag?: string }) {
setIsSubmitting(true) setIsSubmitting(true)
const result: any = await createContract({ const result: any = await createContract(
removeUndefinedProps({
question, question,
outcomeType, outcomeType,
description, description,
initialProb, initialProb,
ante, ante,
closeTime, closeTime,
tags, tags: category ? [category] : undefined,
}).then((r) => r.data || {}) })
).then((r) => r.data || {})
if (result.status !== 'success') { if (result.status !== 'success') {
console.log('error creating contract', result) console.log('error creating contract', result)
@ -199,25 +205,29 @@ export function NewContract(props: { question: string; tag?: string }) {
/> />
</div> </div>
{/* <Spacer h={4} /> <Spacer h={4} />
<div className="form-control max-w-sm items-start"> <div className="form-control max-w-sm items-start">
<label className="label gap-2"> <label className="label gap-2">
<span className="mb-1">Tags</span> <span className="mb-1">Category</span>
<InfoTooltip text="Optional. Help categorize your market with related tags." />
</label> </label>
<input <select
placeholder="e.g. Politics, Economics..." className="select select-bordered w-full max-w-xs"
className="input input-bordered resize-none" onChange={(e) =>
disabled={isSubmitting} setCategory(TO_CATEGORY[e.currentTarget.value] ?? '')
value={tagText} }
onChange={(e) => setTagText(e.target.value || '')} >
/> <option selected={category === ''}></option>
</div> */}
{CATEGORY_LIST.map((cat) => (
<option selected={category === cat} value={CATEGORIES[cat]}>
{CATEGORIES[cat]}
</option>
))}
</select>
</div>
<Spacer h={4} />
<TagsList tags={tags} noLink noLabel />
<Spacer h={4} /> <Spacer h={4} />
<div className="form-control mb-1 items-start"> <div className="form-control mb-1 items-start">

View File

@ -11,6 +11,7 @@ import { useUser } from 'web/hooks/use-user'
import { LoadingIndicator } from 'web/components/loading-indicator' import { LoadingIndicator } from 'web/components/loading-indicator'
import { useAlgoFeed } from 'web/hooks/use-algo-feed' import { useAlgoFeed } from 'web/hooks/use-algo-feed'
import { ContractPageContent } from './[username]/[contractSlug]' import { ContractPageContent } from './[username]/[contractSlug]'
import { CategorySelector } from '../components/feed/category-selector'
const Home = () => { const Home = () => {
const user = useUser() const user = useUser()
@ -42,7 +43,12 @@ const Home = () => {
<Col className="items-center"> <Col className="items-center">
<Col className="w-full max-w-[700px]"> <Col className="w-full max-w-[700px]">
<FeedCreate user={user ?? undefined} /> <FeedCreate user={user ?? undefined} />
<Spacer h={10} /> <Spacer h={2} />
<CategorySelector user={user} />
<Spacer h={1} />
{feed ? ( {feed ? (
<ActivityFeed <ActivityFeed
feed={feed} feed={feed}

View File

@ -140,7 +140,7 @@
core-js-pure "^3.20.2" core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2": "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2":
version "7.17.9" version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
@ -958,6 +958,11 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@react-query-firebase/firestore@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@react-query-firebase/firestore/-/firestore-0.4.2.tgz#6ae52768715aa0a5c0d903dd4fd953ed417ba635"
integrity sha512-7eYp905+sfBRcBTdj7W7BAc3bI3V0D0kKca4/juOTnN4gyoNyaCNOCjLPY467dTq325hGs7BX0ol7Pw3JENdHA==
"@react-spring/animated@~9.2.6-beta.0": "@react-spring/animated@~9.2.6-beta.0":
version "9.2.6" version "9.2.6"
resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e" resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.2.6.tgz#58f30fb75d8bfb7ccbc156cfd6b974a8f3dfd54e"
@ -1535,6 +1540,11 @@ base64-js@^1.3.0:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
big-integer@^1.6.16:
version "1.6.51"
resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
bignumber.js@^9.0.0: bignumber.js@^9.0.0:
version "9.0.2" version "9.0.2"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.2.tgz#71c6c6bed38de64e24a65ebe16cfcf23ae693673"
@ -1588,6 +1598,20 @@ braces@^3.0.1, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
broadcast-channel@^3.4.1:
version "3.7.0"
resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937"
integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==
dependencies:
"@babel/runtime" "^7.7.2"
detect-node "^2.1.0"
js-sha3 "0.8.0"
microseconds "0.2.0"
nano-time "1.0.0"
oblivious-set "1.0.0"
rimraf "3.0.2"
unload "2.2.0"
browserslist@^4.16.6: browserslist@^4.16.6:
version "4.19.1" version "4.19.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
@ -2128,6 +2152,11 @@ destroy@~1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
detect-node@^2.0.4, detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==
detective@^5.2.0: detective@^5.2.0:
version "5.2.0" version "5.2.0"
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
@ -3475,6 +3504,11 @@ jose@^2.0.5:
dependencies: dependencies:
"@panva/asn1.js" "^1.0.0" "@panva/asn1.js" "^1.0.0"
js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -3827,6 +3861,14 @@ make-dir@^3.0.0:
dependencies: dependencies:
semver "^6.0.0" semver "^6.0.0"
match-sorter@^6.0.2:
version "6.3.1"
resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda"
integrity sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==
dependencies:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -3860,6 +3902,11 @@ micromatch@^4.0.4:
braces "^3.0.1" braces "^3.0.1"
picomatch "^2.2.3" picomatch "^2.2.3"
microseconds@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39"
integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==
mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": mime-db@1.51.0, "mime-db@>= 1.43.0 < 2":
version "1.51.0" version "1.51.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
@ -3957,6 +4004,13 @@ multimatch@^4.0.0:
arrify "^2.0.1" arrify "^2.0.1"
minimatch "^3.0.4" minimatch "^3.0.4"
nano-time@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef"
integrity sha1-sFVPaa2J4i0JB/ehKwmTpdlhN+8=
dependencies:
big-integer "^1.6.16"
nanoid@^3.1.23, nanoid@^3.2.0: nanoid@^3.1.23, nanoid@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
@ -4139,6 +4193,11 @@ object.values@^1.1.5:
define-properties "^1.1.3" define-properties "^1.1.3"
es-abstract "^1.19.1" es-abstract "^1.19.1"
oblivious-set@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566"
integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==
on-finished@~2.3.0: on-finished@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -4695,6 +4754,15 @@ react-motion@^0.5.2:
prop-types "^15.5.8" prop-types "^15.5.8"
raf "^3.1.0" raf "^3.1.0"
react-query@^3.39.0:
version "3.39.0"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.0.tgz#0caca7b0da98e65008bbcd4df0d25618c2100050"
integrity sha512-Od0IkSuS79WJOhzWBx/ys0x13+7wFqgnn64vBqqAAnZ9whocVhl/y1padD5uuZ6EIkXbFbInax0qvY7zGM0thA==
dependencies:
"@babel/runtime" "^7.5.5"
broadcast-channel "^3.4.1"
match-sorter "^6.0.2"
react-with-forwarded-ref@^0.3.3: react-with-forwarded-ref@^0.3.3:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5" resolved "https://registry.yarnpkg.com/react-with-forwarded-ref/-/react-with-forwarded-ref-0.3.4.tgz#b1e884ea081ec3c5dd578f37889159797454c0a5"
@ -4767,6 +4835,11 @@ regexpp@^3.2.0:
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
remove-accents@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U=
require-directory@^2.1.1: require-directory@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -4830,7 +4903,7 @@ rfdc@^1.3.0:
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
@ -5421,6 +5494,14 @@ unique-string@^2.0.0:
dependencies: dependencies:
crypto-random-string "^2.0.0" crypto-random-string "^2.0.0"
unload@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7"
integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==
dependencies:
"@babel/runtime" "^7.6.2"
detect-node "^2.0.4"
unpipe@1.0.0, unpipe@~1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"