Merge branch 'main' into range-markets
This commit is contained in:
commit
852a4c7614
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
|||
.idea/
|
||||
.vercel
|
||||
node_modules
|
||||
yarn-error.log
|
||||
|
|
25
common/categories.ts
Normal file
25
common/categories.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
export const CATEGORIES = {
|
||||
politics: 'Politics',
|
||||
technology: 'Technology',
|
||||
sports: 'Sports',
|
||||
gaming: 'Gaming',
|
||||
manifold: 'Manifold',
|
||||
science: 'Science',
|
||||
world: 'World',
|
||||
fun: 'Fun',
|
||||
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)
|
|
@ -5,6 +5,7 @@ export type Comment = {
|
|||
contractId: string
|
||||
betId?: string
|
||||
answerOutcome?: string
|
||||
replyToCommentId?: string
|
||||
userId: string
|
||||
|
||||
text: string
|
||||
|
|
9
common/feed.ts
Normal file
9
common/feed.ts
Normal 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[]
|
||||
}[]
|
|
@ -17,6 +17,8 @@ export type User = {
|
|||
totalDeposits: number
|
||||
totalPnLCached: number
|
||||
creatorVolumeCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
}
|
||||
|
||||
export const STARTING_BALANCE = 1000
|
||||
|
|
|
@ -16,7 +16,7 @@ service cloud.firestore {
|
|||
allow read;
|
||||
allow update: if resource.data.id == request.auth.uid
|
||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle']);
|
||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
|
||||
}
|
||||
|
||||
match /private-users/{userId} {
|
||||
|
@ -35,7 +35,7 @@ service cloud.firestore {
|
|||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
},
|
||||
"main": "lib/functions/src/index.js",
|
||||
"dependencies": {
|
||||
"@react-query-firebase/firestore": "0.4.2",
|
||||
"fetch": "1.1.0",
|
||||
"firebase-admin": "10.0.0",
|
||||
"firebase-functions": "3.16.0",
|
||||
"lodash": "4.17.21",
|
||||
"mailgun-js": "0.22.0",
|
||||
"react-query": "3.39.0",
|
||||
"module-alias": "2.2.2",
|
||||
"stripe": "8.194.0"
|
||||
},
|
||||
|
|
|
@ -7,8 +7,8 @@ import { getNewMultiBetInfo } from 'common/new-bet'
|
|||
import { Answer, MAX_ANSWER_LENGTH } from 'common/answer'
|
||||
import { getContract, getValues } from './utils'
|
||||
import { sendNewAnswerEmail } from './emails'
|
||||
import { Bet } from 'common/bet'
|
||||
import { hasUserHitManaLimit } from 'common/calculate'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { hasUserHitManaLimit } from '../../common/calculate'
|
||||
|
||||
export const createAnswer = functions.runWith({ minInstances: 1 }).https.onCall(
|
||||
async (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
import * as _ from 'lodash'
|
||||
|
||||
import { chargeUser, getUser } from './utils'
|
||||
import {
|
||||
Binary,
|
||||
|
|
71
functions/src/get-feed-data.ts
Normal file
71
functions/src/get-feed-data.ts
Normal 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,
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ import { User } from 'common/user'
|
|||
import { batchedWaitAll } from 'common/util/promise'
|
||||
import { Contract } from 'common/contract'
|
||||
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()
|
||||
|
||||
|
@ -19,8 +21,7 @@ async function updateFeed() {
|
|||
const contracts = await getValues<Contract>(firestore.collection('contracts'))
|
||||
const feedContracts = await getFeedContracts()
|
||||
const users = await getValues<User>(
|
||||
firestore.collection('users')
|
||||
// .where('username', '==', 'JamesGrugett')
|
||||
firestore.collection('users').where('username', '==', 'JamesGrugett')
|
||||
)
|
||||
|
||||
await batchedWaitAll(
|
||||
|
@ -28,7 +29,22 @@ async function updateFeed() {
|
|||
console.log('Updating recs for', user.username)
|
||||
await updateWordScores(user, contracts)
|
||||
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 })
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,15 +10,19 @@ import {
|
|||
getProbability,
|
||||
getOutcomeProbability,
|
||||
getTopAnswer,
|
||||
} from 'common/calculate'
|
||||
import { Bet } from 'common/bet'
|
||||
import { Comment } from 'common/comment'
|
||||
import { User } from 'common/user'
|
||||
} from '../../common/calculate'
|
||||
import { User } from '../../common/user'
|
||||
import {
|
||||
getContractScore,
|
||||
MAX_FEED_CONTRACTS,
|
||||
} from 'common/recommended-contracts'
|
||||
import { callCloudFunction } from './call-cloud-function'
|
||||
import {
|
||||
getFeedContracts,
|
||||
getRecentBetsAndComments,
|
||||
getTaggedContracts,
|
||||
} from './get-feed-data'
|
||||
import { CATEGORY_LIST } from '../../common/categories'
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
|
@ -28,16 +32,28 @@ export const updateFeed = functions.pubsub
|
|||
const users = await getValues<User>(firestore.collection('users'))
|
||||
|
||||
const batchSize = 100
|
||||
const userBatches: User[][] = []
|
||||
let userBatches: User[][] = []
|
||||
for (let i = 0; i < users.length; i += batchSize) {
|
||||
userBatches.push(users.slice(i, i + batchSize))
|
||||
}
|
||||
|
||||
console.log('updating feed batch')
|
||||
|
||||
await Promise.all(
|
||||
userBatches.map(async (users) =>
|
||||
userBatches.map((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(
|
||||
|
@ -45,40 +61,56 @@ export const updateFeedBatch = functions.https.onCall(
|
|||
const { users } = data
|
||||
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() {
|
||||
// Get contracts bet on or created in last week.
|
||||
const contracts = await Promise.all([
|
||||
getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('isResolved', '==', false)
|
||||
.where('volume7Days', '>', 0)
|
||||
),
|
||||
export const updateCategoryFeedBatch = functions.https.onCall(
|
||||
async (data: { users: User[]; category: string }) => {
|
||||
const { users, category } = data
|
||||
const contracts = await getTaggedContracts(category)
|
||||
|
||||
getValues<Contract>(
|
||||
firestore
|
||||
.collection('contracts')
|
||||
.where('isResolved', '==', false)
|
||||
.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())
|
||||
await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const feed = await computeFeed(user, contracts)
|
||||
await getUserCacheCollection(user).doc(`feed-${category}`).set({ feed })
|
||||
})
|
||||
|
||||
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([
|
||||
getValue<{ [word: string]: number }>(userCacheCollection.doc('wordScores')),
|
||||
getValue<{ [contractId: string]: number }>(
|
||||
|
@ -109,8 +141,7 @@ export const doUserFeedUpdate = async (user: User, contracts: Contract[]) => {
|
|||
const feed = await Promise.all(
|
||||
feedContracts.map((contract) => getRecentBetsAndComments(contract))
|
||||
)
|
||||
|
||||
await userCacheCollection.doc('feed').set({ feed })
|
||||
return feed
|
||||
}
|
||||
|
||||
function scoreContract(
|
||||
|
@ -180,31 +211,3 @@ function getLastViewedScore(viewTime: number | undefined) {
|
|||
const frac = logInterpolation(0.5, 14, daysAgo)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,6 +303,7 @@ function BuyPanel(props: {
|
|||
<>
|
||||
<YesNoSelector
|
||||
className="mb-4"
|
||||
btnClassName="flex-1"
|
||||
selected={betChoice}
|
||||
onSelect={(choice) => onBetChoice(choice)}
|
||||
/>
|
||||
|
|
|
@ -15,9 +15,9 @@ import { useSaveShares } from './use-save-shares'
|
|||
export default function BetRow(props: {
|
||||
contract: FullContract<DPM | CPMM, Binary>
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
btnClassName?: string
|
||||
}) {
|
||||
const { className, labelClassName, contract } = props
|
||||
const { className, btnClassName, contract } = props
|
||||
const [open, setOpen] = useState(false)
|
||||
const [betChoice, setBetChoice] = useState<'YES' | 'NO' | undefined>(
|
||||
undefined
|
||||
|
@ -31,12 +31,9 @@ export default function BetRow(props: {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Row className={clsx('mt-2 justify-end space-x-3', className)}>
|
||||
{/* <div className={clsx('mr-2 text-gray-400', labelClassName)}>
|
||||
Place a trade
|
||||
</div> */}
|
||||
<YesNoSelector
|
||||
btnClassName="btn-sm w-24"
|
||||
className={clsx('mt-2 justify-end', className)}
|
||||
btnClassName={clsx('btn-sm w-24', btnClassName)}
|
||||
onSelect={(choice) => {
|
||||
setOpen(true)
|
||||
setBetChoice(choice)
|
||||
|
@ -62,7 +59,6 @@ export default function BetRow(props: {
|
|||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Row>
|
||||
<Modal open={open} setOpen={setOpen}>
|
||||
<BetPanelSwitcher
|
||||
contract={contract}
|
||||
|
|
|
@ -2,6 +2,7 @@ import clsx from 'clsx'
|
|||
import dayjs from 'dayjs'
|
||||
import { useState } from 'react'
|
||||
import Textarea from 'react-expanding-textarea'
|
||||
import { CATEGORY_LIST } from '../../../common/categories'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
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 { Row } from '../layout/row'
|
||||
import { Linkify } from '../linkify'
|
||||
import { TagsList } from '../tags-list'
|
||||
|
||||
export function ContractDescription(props: {
|
||||
contract: Contract
|
||||
|
@ -26,6 +28,7 @@ export function ContractDescription(props: {
|
|||
`${newDescription} ${contract.tags.map((tag) => `#${tag}`).join(' ')}`
|
||||
)
|
||||
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
|
||||
|
||||
await updateContract(contract.id, {
|
||||
description: newDescription,
|
||||
tags,
|
||||
|
@ -35,6 +38,9 @@ export function ContractDescription(props: {
|
|||
|
||||
if (!isCreator && !contract.description.trim()) return null
|
||||
|
||||
const { tags } = contract
|
||||
const category = tags.find((tag) => CATEGORY_LIST.includes(tag.toLowerCase()))
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
|
@ -43,7 +49,15 @@ export function ContractDescription(props: {
|
|||
)}
|
||||
>
|
||||
<Linkify text={contract.description} />
|
||||
|
||||
{category && (
|
||||
<div className="mt-4">
|
||||
<TagsList tags={[category]} label="Category" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<br />
|
||||
|
||||
{isCreator && (
|
||||
<EditContract
|
||||
// Note: Because descriptionTimestamp is called once, later edits use
|
||||
|
|
|
@ -60,9 +60,7 @@ export const ContractOverview = (props: {
|
|||
<Row className="items-center justify-between gap-4 xl:hidden">
|
||||
<BinaryResolutionOrChance contract={contract} />
|
||||
|
||||
{tradingAllowed(contract) && (
|
||||
<BetRow contract={contract} labelClassName="hidden" />
|
||||
)}
|
||||
{tradingAllowed(contract) && <BetRow contract={contract} />}
|
||||
</Row>
|
||||
) : (
|
||||
outcomeType === 'FREE_RESPONSE' &&
|
||||
|
|
|
@ -20,11 +20,11 @@ export function ActivityFeed(props: {
|
|||
const user = useUser()
|
||||
|
||||
return (
|
||||
<Col className="divide-y divide-gray-300 bg-white">
|
||||
<Col className="gap-2">
|
||||
{feed.map((item) => (
|
||||
<ContractActivity
|
||||
key={item.contract.id}
|
||||
className="py-6 px-2 sm:px-4"
|
||||
className="rounded-md bg-white py-6 px-2 sm:px-4"
|
||||
user={user}
|
||||
contract={item.contract}
|
||||
bets={item.recentBets}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import _ from 'lodash'
|
||||
import _, { Dictionary } from 'lodash'
|
||||
|
||||
import { Answer } from 'common/answer'
|
||||
import { Bet } from 'common/bet'
|
||||
|
@ -18,6 +18,7 @@ export type ActivityItem =
|
|||
| CloseItem
|
||||
| ResolveItem
|
||||
| CommentInputItem
|
||||
| CommentThreadItem
|
||||
|
||||
type BaseActivityItem = {
|
||||
id: string
|
||||
|
@ -53,9 +54,15 @@ export type CommentItem = BaseActivityItem & {
|
|||
type: 'comment'
|
||||
comment: Comment
|
||||
betsBySameUser: Bet[]
|
||||
hideOutcome: boolean
|
||||
truncate: boolean
|
||||
smallAvatar: boolean
|
||||
truncate?: boolean
|
||||
smallAvatar?: boolean
|
||||
}
|
||||
|
||||
export type CommentThreadItem = BaseActivityItem & {
|
||||
type: 'commentThread'
|
||||
parentComment: Comment
|
||||
comments: Comment[]
|
||||
betsByUserId: Dictionary<[Bet, ...Bet[]]>
|
||||
}
|
||||
|
||||
export type BetGroupItem = BaseActivityItem & {
|
||||
|
@ -68,6 +75,8 @@ export type AnswerGroupItem = BaseActivityItem & {
|
|||
type: 'answergroup' | 'answer'
|
||||
answer: Answer
|
||||
items: ActivityItem[]
|
||||
betsByCurrentUser?: Bet[]
|
||||
comments?: Comment[]
|
||||
}
|
||||
|
||||
export type CloseItem = BaseActivityItem & {
|
||||
|
@ -131,7 +140,6 @@ function groupBets(
|
|||
comment,
|
||||
betsBySameUser: [bet],
|
||||
contract,
|
||||
hideOutcome,
|
||||
truncate: abbreviated,
|
||||
smallAvatar,
|
||||
}
|
||||
|
@ -273,41 +281,21 @@ function getAnswerAndCommentInputGroups(
|
|||
getOutcomeProbability(contract, outcome)
|
||||
)
|
||||
|
||||
function collateCommentsSectionForOutcome(outcome: string) {
|
||||
const answerBets = bets.filter((bet) => bet.outcome === outcome)
|
||||
const answerComments = comments.filter(
|
||||
(comment) =>
|
||||
comment.answerOutcome === outcome ||
|
||||
answerBets.some((bet) => bet.id === comment.betId)
|
||||
)
|
||||
let items = []
|
||||
items.push({
|
||||
type: 'commentInput' as const,
|
||||
id: 'commentInputFor' + outcome,
|
||||
contract,
|
||||
betsByCurrentUser: user
|
||||
? bets.filter((bet) => bet.userId === user.id)
|
||||
: [],
|
||||
comments: comments,
|
||||
answerOutcome: outcome,
|
||||
})
|
||||
items.push(
|
||||
...getCommentsWithPositions(
|
||||
answerBets,
|
||||
answerComments,
|
||||
contract
|
||||
).reverse()
|
||||
)
|
||||
return items
|
||||
}
|
||||
|
||||
const answerGroups = outcomes
|
||||
.map((outcome) => {
|
||||
const answer = contract.answers?.find(
|
||||
(answer) => answer.id === outcome
|
||||
) as Answer
|
||||
|
||||
const items = collateCommentsSectionForOutcome(outcome)
|
||||
const answerBets = bets.filter((bet) => bet.outcome === outcome)
|
||||
const answerComments = comments.filter(
|
||||
(comment) =>
|
||||
comment.answerOutcome === outcome ||
|
||||
answerBets.some((bet) => bet.id === comment.betId)
|
||||
)
|
||||
const items = getCommentThreads(answerBets, answerComments, contract)
|
||||
|
||||
if (outcome === GENERAL_COMMENTS_OUTCOME_ID) items.reverse()
|
||||
|
||||
return {
|
||||
id: outcome,
|
||||
|
@ -316,6 +304,8 @@ function getAnswerAndCommentInputGroups(
|
|||
answer,
|
||||
items,
|
||||
user,
|
||||
betsByCurrentUser: answerBets.filter((bet) => bet.userId === user?.id),
|
||||
comments: answerComments,
|
||||
}
|
||||
})
|
||||
.filter((group) => group.answer) as ActivityItem[]
|
||||
|
@ -344,7 +334,6 @@ function groupBetsAndComments(
|
|||
comment,
|
||||
betsBySameUser: [],
|
||||
truncate: abbreviated,
|
||||
hideOutcome: true,
|
||||
smallAvatar,
|
||||
}))
|
||||
|
||||
|
@ -370,22 +359,21 @@ function groupBetsAndComments(
|
|||
return abbrItems
|
||||
}
|
||||
|
||||
function getCommentsWithPositions(
|
||||
function getCommentThreads(
|
||||
bets: Bet[],
|
||||
comments: Comment[],
|
||||
contract: Contract
|
||||
) {
|
||||
const betsByUserId = _.groupBy(bets, (bet) => bet.userId)
|
||||
const parentComments = comments.filter((comment) => !comment.replyToCommentId)
|
||||
|
||||
const items = comments.map((comment) => ({
|
||||
type: 'comment' as const,
|
||||
const items = parentComments.map((comment) => ({
|
||||
type: 'commentThread' as const,
|
||||
id: comment.id,
|
||||
contract: contract,
|
||||
comment,
|
||||
betsBySameUser: bets.length === 0 ? [] : betsByUserId[comment.userId] ?? [],
|
||||
truncate: false,
|
||||
hideOutcome: false,
|
||||
smallAvatar: false,
|
||||
comments: comments,
|
||||
parentComment: comment,
|
||||
betsByUserId: betsByUserId,
|
||||
}))
|
||||
|
||||
return items
|
||||
|
@ -546,6 +534,8 @@ export function getSpecificContractActivityItems(
|
|||
|
||||
switch (mode) {
|
||||
case 'bets':
|
||||
// Remove first bet (which is the ante):
|
||||
if (contract.outcomeType === 'FREE_RESPONSE') bets = bets.slice(1)
|
||||
items.push(
|
||||
...bets.map((bet) => ({
|
||||
type: 'bet' as const,
|
||||
|
@ -566,7 +556,7 @@ export function getSpecificContractActivityItems(
|
|||
const nonFreeResponseBets =
|
||||
contract.outcomeType === 'FREE_RESPONSE' ? [] : bets
|
||||
items.push(
|
||||
...getCommentsWithPositions(
|
||||
...getCommentThreads(
|
||||
nonFreeResponseBets,
|
||||
nonFreeResponseComments,
|
||||
contract
|
||||
|
|
75
web/components/feed/category-selector.tsx
Normal file
75
web/components/feed/category-selector.tsx
Normal 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 hover:bg-gray-200',
|
||||
'cursor-pointer select-none',
|
||||
isFollowed ? 'border-gray-300 bg-gray-300' : 'bg-white'
|
||||
)}
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="text-sm text-gray-500">{category}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from './activity-items'
|
||||
import { FeedItems } from './feed-items'
|
||||
import { User } from 'common/user'
|
||||
import { useContract } from 'web/hooks/use-contract'
|
||||
|
||||
export function ContractActivity(props: {
|
||||
contract: Contract
|
||||
|
@ -27,8 +28,9 @@ export function ContractActivity(props: {
|
|||
className?: string
|
||||
betRowClassName?: string
|
||||
}) {
|
||||
const { contract, user, mode, contractPath, className, betRowClassName } =
|
||||
props
|
||||
const { user, mode, contractPath, className, betRowClassName } = props
|
||||
|
||||
const contract = useContract(props.contract.id) ?? props.contract
|
||||
|
||||
const updatedComments =
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// From https://tailwindui.com/components/application-ui/lists/feeds
|
||||
import React, { Fragment, useRef, useState } from 'react'
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import * as _ from 'lodash'
|
||||
import { Dictionary } from 'lodash'
|
||||
import {
|
||||
BanIcon,
|
||||
CheckIcon,
|
||||
|
@ -15,8 +16,8 @@ import Textarea from 'react-expanding-textarea'
|
|||
|
||||
import { OutcomeLabel } from '../outcome-label'
|
||||
import {
|
||||
contractMetrics,
|
||||
Contract,
|
||||
contractMetrics,
|
||||
contractPath,
|
||||
tradingAllowed,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
|
@ -30,15 +31,13 @@ import { BinaryResolutionOrChance } from '../contract/contract-card'
|
|||
import { SiteLink } from '../site-link'
|
||||
import { Col } from '../layout/col'
|
||||
import { UserLink } from '../user-page'
|
||||
import { DateTimeTooltip } from '../datetime-tooltip'
|
||||
import { Bet } from 'web/lib/firebase/bets'
|
||||
import { JoinSpans } from '../join-spans'
|
||||
import { fromNow } from 'web/lib/util/time'
|
||||
import BetRow from '../bet-row'
|
||||
import { Avatar } from '../avatar'
|
||||
import { Answer } from 'common/answer'
|
||||
import { ActivityItem, GENERAL_COMMENTS_OUTCOME_ID } from './activity-items'
|
||||
import { Binary, CPMM, DPM, FreeResponse, FullContract } from 'common/contract'
|
||||
import { Binary, CPMM, FreeResponse, FullContract } from 'common/contract'
|
||||
import { BuyButton } from '../yes-no-selector'
|
||||
import { getDpmOutcomeProbability } from 'common/calculate-dpm'
|
||||
import { AnswerBetPanel } from '../answers/answer-bet-panel'
|
||||
|
@ -118,24 +117,97 @@ function FeedItem(props: { item: ActivityItem }) {
|
|||
return <FeedResolve {...item} />
|
||||
case 'commentInput':
|
||||
return <CommentInput {...item} />
|
||||
case 'commentThread':
|
||||
return <FeedCommentThread {...item} />
|
||||
}
|
||||
}
|
||||
|
||||
export function FeedCommentThread(props: {
|
||||
contract: Contract
|
||||
comments: Comment[]
|
||||
parentComment: Comment
|
||||
betsByUserId: Dictionary<[Bet, ...Bet[]]>
|
||||
truncate?: boolean
|
||||
smallAvatar?: boolean
|
||||
}) {
|
||||
const {
|
||||
contract,
|
||||
comments,
|
||||
betsByUserId,
|
||||
truncate,
|
||||
smallAvatar,
|
||||
parentComment,
|
||||
} = props
|
||||
const [showReply, setShowReply] = useState(false)
|
||||
const [replyToUsername, setReplyToUsername] = useState('')
|
||||
const user = useUser()
|
||||
const commentsList = comments.filter(
|
||||
(comment) => comment.replyToCommentId === parentComment.id
|
||||
)
|
||||
commentsList.unshift(parentComment)
|
||||
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
||||
function scrollAndOpenReplyInput(comment: Comment) {
|
||||
setReplyToUsername(comment.userUsername)
|
||||
setShowReply(true)
|
||||
inputRef?.focus()
|
||||
}
|
||||
useEffect(() => {
|
||||
if (showReply && inputRef) inputRef.focus()
|
||||
}, [inputRef, showReply])
|
||||
return (
|
||||
<div className={'w-full flex-col flex-col pr-6'}>
|
||||
{commentsList.map((comment, commentIdx) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
id={comment.id}
|
||||
className={clsx(
|
||||
'flex space-x-3',
|
||||
commentIdx === 0 ? '' : 'mt-4 ml-8'
|
||||
)}
|
||||
>
|
||||
<FeedComment
|
||||
contract={contract}
|
||||
comment={comment}
|
||||
betsBySameUser={betsByUserId[comment.userId] ?? []}
|
||||
onReplyClick={scrollAndOpenReplyInput}
|
||||
smallAvatar={smallAvatar}
|
||||
truncate={truncate}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{showReply && (
|
||||
<div className={'ml-8 w-full pt-6'}>
|
||||
<CommentInput
|
||||
contract={contract}
|
||||
// Should we allow replies to contain recent bet info?
|
||||
betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
|
||||
comments={comments}
|
||||
parentComment={parentComment}
|
||||
replyToUsername={replyToUsername}
|
||||
answerOutcome={comments[0].answerOutcome}
|
||||
setRef={setInputRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeedComment(props: {
|
||||
contract: Contract
|
||||
comment: Comment
|
||||
betsBySameUser: Bet[]
|
||||
hideOutcome: boolean
|
||||
truncate: boolean
|
||||
smallAvatar: boolean
|
||||
truncate?: boolean
|
||||
smallAvatar?: boolean
|
||||
onReplyClick?: (comment: Comment) => void
|
||||
}) {
|
||||
const {
|
||||
contract,
|
||||
comment,
|
||||
betsBySameUser,
|
||||
hideOutcome,
|
||||
truncate,
|
||||
smallAvatar,
|
||||
onReplyClick,
|
||||
} = props
|
||||
const { text, userUsername, userName, userAvatarUrl, createdTime } = comment
|
||||
let outcome: string | undefined,
|
||||
|
@ -187,7 +259,7 @@ export function FeedComment(props: {
|
|||
)}
|
||||
<>
|
||||
{bought} {money}
|
||||
{outcome && !hideOutcome && (
|
||||
{contract.outcomeType !== 'FREE_RESPONSE' && outcome && (
|
||||
<>
|
||||
{' '}
|
||||
of{' '}
|
||||
|
@ -206,6 +278,14 @@ export function FeedComment(props: {
|
|||
moreHref={contractPath(contract)}
|
||||
shouldTruncate={truncate}
|
||||
/>
|
||||
{onReplyClick && (
|
||||
<button
|
||||
className={'text-xs font-bold text-gray-500 hover:underline'}
|
||||
onClick={() => onReplyClick(comment)}
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -215,59 +295,79 @@ export function CommentInput(props: {
|
|||
contract: Contract
|
||||
betsByCurrentUser: Bet[]
|
||||
comments: Comment[]
|
||||
// Only for free response comment inputs
|
||||
// Tie a comment to an free response answer outcome
|
||||
answerOutcome?: string
|
||||
// Tie a comment to another comment
|
||||
parentComment?: Comment
|
||||
replyToUsername?: string
|
||||
setRef?: (ref: HTMLTextAreaElement) => void
|
||||
}) {
|
||||
const { contract, betsByCurrentUser, comments, answerOutcome } = props
|
||||
const {
|
||||
contract,
|
||||
betsByCurrentUser,
|
||||
comments,
|
||||
answerOutcome,
|
||||
parentComment,
|
||||
replyToUsername,
|
||||
setRef,
|
||||
} = props
|
||||
const user = useUser()
|
||||
const [comment, setComment] = useState('')
|
||||
const [focused, setFocused] = useState(false)
|
||||
|
||||
// Should this be oldest bet or most recent bet?
|
||||
const mostRecentCommentableBet = betsByCurrentUser
|
||||
.filter((bet) => {
|
||||
if (
|
||||
canCommentOnBet(bet, user) &&
|
||||
// The bet doesn't already have a comment
|
||||
!comments.some((comment) => comment.betId == bet.id)
|
||||
) {
|
||||
if (!answerOutcome) return true
|
||||
// If we're in free response, don't allow commenting on ante bet
|
||||
return (
|
||||
bet.outcome !== GENERAL_COMMENTS_OUTCOME_ID &&
|
||||
answerOutcome === bet.outcome
|
||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
||||
betsByCurrentUser,
|
||||
comments,
|
||||
user,
|
||||
answerOutcome
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||
.pop()
|
||||
|
||||
const { id } = mostRecentCommentableBet || { id: undefined }
|
||||
|
||||
useEffect(() => {
|
||||
if (!replyToUsername || !user || replyToUsername === user.username) return
|
||||
const replacement = `@${replyToUsername} `
|
||||
setComment(replacement + comment.replace(replacement, ''))
|
||||
}, [user, replyToUsername])
|
||||
|
||||
async function submitComment(betId: string | undefined) {
|
||||
if (!user) {
|
||||
return await firebaseLogin()
|
||||
}
|
||||
if (!comment) return
|
||||
await createComment(contract.id, comment, user, betId, answerOutcome)
|
||||
|
||||
// Update state asap to avoid double submission.
|
||||
const commentValue = comment.toString()
|
||||
setComment('')
|
||||
await createComment(
|
||||
contract.id,
|
||||
commentValue,
|
||||
user,
|
||||
betId,
|
||||
answerOutcome,
|
||||
parentComment?.id
|
||||
)
|
||||
}
|
||||
|
||||
const { userPosition, userPositionMoney, yesFloorShares, noFloorShares } =
|
||||
getBettorsPosition(contract, Date.now(), betsByCurrentUser)
|
||||
|
||||
const shouldCollapseAfterClickOutside = false
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row className={'flex w-full gap-2'}>
|
||||
<Row className={'mb-2 flex w-full gap-2'}>
|
||||
<div className={'mt-1'}>
|
||||
<Avatar avatarUrl={user?.avatarUrl} username={user?.username} />
|
||||
</div>
|
||||
<div className={'min-w-0 flex-1'}>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className={'mb-1'}>
|
||||
{mostRecentCommentableBet && (
|
||||
<BetStatusText
|
||||
contract={contract}
|
||||
bet={mostRecentCommentableBet}
|
||||
isSelf={true}
|
||||
hideOutcome={contract.outcomeType === 'FREE_RESPONSE'}
|
||||
/>
|
||||
)}
|
||||
{!mostRecentCommentableBet && user && userPosition > 0 && (
|
||||
|
@ -283,67 +383,73 @@ export function CommentInput(props: {
|
|||
</>
|
||||
</>
|
||||
)}
|
||||
{(answerOutcome === undefined || focused) && (
|
||||
<div className="mt-2">
|
||||
</div>
|
||||
|
||||
<Row className="gap-1.5">
|
||||
<Textarea
|
||||
ref={(ref: HTMLTextAreaElement) => setRef?.(ref)}
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
className="textarea textarea-bordered w-full resize-none"
|
||||
placeholder="Add a comment..."
|
||||
placeholder={
|
||||
parentComment || answerOutcome
|
||||
? 'Write a reply... '
|
||||
: 'Write a comment...'
|
||||
}
|
||||
autoFocus={focused}
|
||||
rows={answerOutcome == undefined || focused ? 3 : 1}
|
||||
rows={focused ? 3 : 1}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => !comment && setFocused(false)}
|
||||
onBlur={() =>
|
||||
shouldCollapseAfterClickOutside && setFocused(false)
|
||||
}
|
||||
maxLength={MAX_COMMENT_LENGTH}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
submitComment(id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex justify-center',
|
||||
focused ? 'items-end' : 'items-center'
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
{!user && (
|
||||
<button
|
||||
className={
|
||||
'btn btn-outline btn-sm text-transform: mt-1 capitalize'
|
||||
'btn btn-outline btn-sm text-transform: capitalize'
|
||||
}
|
||||
onClick={() => submitComment(id)}
|
||||
>
|
||||
Sign in to Comment
|
||||
</button>
|
||||
)}
|
||||
{user && answerOutcome === undefined && (
|
||||
{user && (
|
||||
<button
|
||||
className={
|
||||
'btn btn-outline btn-sm text-transform: mt-1 capitalize'
|
||||
}
|
||||
onClick={() => submitComment(id)}
|
||||
>
|
||||
Comment
|
||||
</button>
|
||||
className={clsx(
|
||||
'btn text-transform: block capitalize',
|
||||
focused && comment
|
||||
? 'btn-outline btn-sm '
|
||||
: 'btn-ghost btn-sm text-gray-500'
|
||||
)}
|
||||
{user && answerOutcome !== undefined && (
|
||||
<button
|
||||
className={
|
||||
focused
|
||||
? 'btn btn-outline btn-sm text-transform: mt-1 capitalize'
|
||||
: 'btn btn-ghost btn-sm text-transform: mt-1 capitalize'
|
||||
}
|
||||
onClick={() => {
|
||||
if (!focused) setFocused(true)
|
||||
if (!focused) return
|
||||
else {
|
||||
submitComment(id)
|
||||
setFocused(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!focused ? 'Add Comment' : 'Comment'}
|
||||
{parentComment || answerOutcome ? 'Reply' : 'Comment'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -435,8 +541,8 @@ export function FeedBet(props: {
|
|||
bet={bet}
|
||||
contract={contract}
|
||||
isSelf={isSelf}
|
||||
hideOutcome={hideOutcome}
|
||||
bettor={bettor}
|
||||
hideOutcome={hideOutcome}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
|
@ -448,10 +554,10 @@ function BetStatusText(props: {
|
|||
contract: Contract
|
||||
bet: Bet
|
||||
isSelf: boolean
|
||||
hideOutcome?: boolean
|
||||
bettor?: User
|
||||
hideOutcome?: boolean
|
||||
}) {
|
||||
const { bet, contract, hideOutcome, bettor, isSelf } = props
|
||||
const { bet, contract, bettor, isSelf, hideOutcome } = props
|
||||
const { amount, outcome, createdTime } = bet
|
||||
|
||||
const bought = amount >= 0 ? 'bought' : 'sold'
|
||||
|
@ -581,6 +687,32 @@ export function FeedQuestion(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function getMostRecentCommentableBet(
|
||||
betsByCurrentUser: Bet[],
|
||||
comments: Comment[],
|
||||
user?: User | null,
|
||||
answerOutcome?: string
|
||||
) {
|
||||
return betsByCurrentUser
|
||||
.filter((bet) => {
|
||||
if (
|
||||
canCommentOnBet(bet, user) &&
|
||||
// The bet doesn't already have a comment
|
||||
!comments.some((comment) => comment.betId == bet.id)
|
||||
) {
|
||||
if (!answerOutcome) return true
|
||||
// If we're in free response, don't allow commenting on ante bet
|
||||
return (
|
||||
bet.outcome !== GENERAL_COMMENTS_OUTCOME_ID &&
|
||||
answerOutcome === bet.outcome
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
.sort((b1, b2) => b1.createdTime - b2.createdTime)
|
||||
.pop()
|
||||
}
|
||||
|
||||
function canCommentOnBet(bet: Bet, user?: User | null) {
|
||||
const { userId, createdTime, isRedemption } = bet
|
||||
const isSelf = user?.id === userId
|
||||
|
@ -768,13 +900,35 @@ function FeedAnswerGroup(props: {
|
|||
answer: Answer
|
||||
items: ActivityItem[]
|
||||
type: string
|
||||
betsByCurrentUser?: Bet[]
|
||||
comments?: Comment[]
|
||||
}) {
|
||||
const { answer, items, contract, type } = props
|
||||
const { answer, items, contract, type, betsByCurrentUser, comments } = props
|
||||
const { username, avatarUrl, name, text } = answer
|
||||
|
||||
const user = useUser()
|
||||
const mostRecentCommentableBet = getMostRecentCommentableBet(
|
||||
betsByCurrentUser ?? [],
|
||||
comments ?? [],
|
||||
user,
|
||||
answer.number + ''
|
||||
)
|
||||
const prob = getDpmOutcomeProbability(contract.totalShares, answer.id)
|
||||
const probPercent = formatPercent(prob)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showReply, setShowReply] = useState(false)
|
||||
const isFreeResponseContractPage = type === 'answergroup' && comments
|
||||
if (mostRecentCommentableBet && !showReply) setShowReplyAndFocus(true)
|
||||
const [inputRef, setInputRef] = useState<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// If they've already opened the input box, focus it once again
|
||||
function setShowReplyAndFocus(show: boolean) {
|
||||
setShowReply(show)
|
||||
inputRef?.focus()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (showReply && inputRef) inputRef.focus()
|
||||
}, [inputRef, showReply])
|
||||
|
||||
return (
|
||||
<Col
|
||||
|
@ -794,11 +948,17 @@ function FeedAnswerGroup(props: {
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
{type == 'answer' && (
|
||||
<div
|
||||
className="absolute -m-3 h-full rounded-tl-md bg-green-600 bg-opacity-10"
|
||||
style={{ width: `${100 * Math.max(prob, 0.01)}%` }}
|
||||
></div>
|
||||
)}
|
||||
<Row className="my-4 gap-3">
|
||||
<div className="px-1">
|
||||
<Avatar username={username} avatarUrl={avatarUrl} />
|
||||
</div>
|
||||
<Col className="min-w-0 flex-1 gap-2">
|
||||
<Col className="min-w-0 flex-1 lg:gap-1">
|
||||
<div className="text-sm text-gray-500">
|
||||
<UserLink username={username} name={name} /> answered
|
||||
</div>
|
||||
|
@ -808,11 +968,25 @@ function FeedAnswerGroup(props: {
|
|||
<Linkify text={text} />
|
||||
</span>
|
||||
|
||||
<Row className="align-items justify-end gap-4">
|
||||
<Row className="items-center justify-center gap-4">
|
||||
{isFreeResponseContractPage && (
|
||||
<div className={'sm:hidden'}>
|
||||
<button
|
||||
className={
|
||||
'text-xs font-bold text-gray-500 hover:underline'
|
||||
}
|
||||
onClick={() => setShowReplyAndFocus(true)}
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={'align-items flex w-full justify-end gap-4 '}>
|
||||
<span
|
||||
className={clsx(
|
||||
'text-2xl',
|
||||
tradingAllowed(contract) ? 'text-green-500' : 'text-gray-500'
|
||||
tradingAllowed(contract) ? 'text-primary' : 'text-gray-500'
|
||||
)}
|
||||
>
|
||||
{probPercent}
|
||||
|
@ -824,8 +998,19 @@ function FeedAnswerGroup(props: {
|
|||
)}
|
||||
onClick={() => setOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
</Row>
|
||||
</Col>
|
||||
{isFreeResponseContractPage && (
|
||||
<div className={'justify-initial hidden sm:block'}>
|
||||
<button
|
||||
className={'text-xs font-bold text-gray-500 hover:underline'}
|
||||
onClick={() => setShowReplyAndFocus(true)}
|
||||
>
|
||||
Reply
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
|
@ -848,6 +1033,19 @@ function FeedAnswerGroup(props: {
|
|||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{showReply && (
|
||||
<div className={'ml-8 pt-4'}>
|
||||
<CommentInput
|
||||
contract={contract}
|
||||
betsByCurrentUser={betsByCurrentUser ?? []}
|
||||
comments={comments ?? []}
|
||||
answerOutcome={answer.number + ''}
|
||||
replyToUsername={answer.username}
|
||||
setRef={setInputRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ export function TagsInput(props: { contract: Contract; className?: string }) {
|
|||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
const updateTags = () => {
|
||||
const updateTags = async () => {
|
||||
setIsSubmitting(true)
|
||||
updateContract(contract.id, {
|
||||
await updateContract(contract.id, {
|
||||
tags: newTags,
|
||||
lowercaseTags: newTags.map((tag) => tag.toLowerCase()),
|
||||
})
|
||||
|
@ -37,6 +37,12 @@ export function TagsInput(props: { contract: Contract; className?: string }) {
|
|||
disabled={isSubmitting}
|
||||
value={tagText}
|
||||
onChange={(e) => setTagText(e.target.value || '')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
updateTags()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button className="btn btn-xs btn-outline" onClick={updateTags}>
|
||||
Save tags
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import { CATEGORIES } from '../../common/categories'
|
||||
import { Col } from './layout/col'
|
||||
|
||||
import { Row } from './layout/row'
|
||||
|
@ -6,6 +7,8 @@ import { SiteLink } from './site-link'
|
|||
|
||||
function Hashtag(props: { tag: string; noLink?: boolean }) {
|
||||
const { tag, noLink } = props
|
||||
const category = CATEGORIES[tag.replace('#', '').toLowerCase()]
|
||||
|
||||
const body = (
|
||||
<div
|
||||
className={clsx(
|
||||
|
@ -13,7 +16,7 @@ function Hashtag(props: { tag: string; noLink?: boolean }) {
|
|||
!noLink && 'cursor-pointer'
|
||||
)}
|
||||
>
|
||||
<span className="text-sm text-gray-600">{tag}</span>
|
||||
<span className="text-sm text-gray-600">{category ?? tag}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
@ -30,11 +33,12 @@ export function TagsList(props: {
|
|||
className?: string
|
||||
noLink?: boolean
|
||||
noLabel?: boolean
|
||||
label?: string
|
||||
}) {
|
||||
const { tags, className, noLink, noLabel } = props
|
||||
const { tags, className, noLink, noLabel, label } = props
|
||||
return (
|
||||
<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) => (
|
||||
<Hashtag
|
||||
key={tag}
|
||||
|
|
|
@ -22,7 +22,7 @@ export function YesNoSelector(props: {
|
|||
} = props
|
||||
|
||||
const commonClassNames =
|
||||
'inline-flex flex-1 items-center justify-center rounded-3xl border-2 p-2'
|
||||
'inline-flex items-center justify-center rounded-3xl border-2 p-2'
|
||||
|
||||
return (
|
||||
<Row className={clsx('space-x-3', className)}>
|
||||
|
|
|
@ -1,26 +1,18 @@
|
|||
import _ from 'lodash'
|
||||
import _, { Dictionary } from 'lodash'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Bet } from 'common/bet'
|
||||
import { Comment } from 'common/comment'
|
||||
import { Contract } from 'common/contract'
|
||||
import type { feed } from 'common/feed'
|
||||
import { useTimeSinceFirstRender } from './use-time-since-first-render'
|
||||
import { trackLatency } from 'web/lib/firebase/tracking'
|
||||
import { User } from 'common/user'
|
||||
import { getUserFeed } from 'web/lib/firebase/users'
|
||||
import { useUpdatedContracts } from './use-contracts'
|
||||
import { getCategoryFeeds, getUserFeed } from 'web/lib/firebase/users'
|
||||
import {
|
||||
getRecentBetsAndComments,
|
||||
getTopWeeklyContracts,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
|
||||
type feed = {
|
||||
contract: Contract
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
}[]
|
||||
|
||||
export const useAlgoFeed = (user: User | null | undefined) => {
|
||||
const [feed, setFeed] = useState<feed>()
|
||||
const [categoryFeeds, setCategoryFeeds] = useState<Dictionary<feed>>()
|
||||
|
||||
const getTime = useTimeSinceFirstRender()
|
||||
|
||||
|
@ -34,21 +26,21 @@ export const useAlgoFeed = (user: User | null | undefined) => {
|
|||
trackLatency('feed', 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])
|
||||
|
||||
return useUpdateFeed(feed)
|
||||
}
|
||||
const followedCategory = user?.followedCategories?.[0] ?? 'all'
|
||||
|
||||
const useUpdateFeed = (feed: feed | undefined) => {
|
||||
const contracts = useUpdatedContracts(feed?.map((item) => item.contract))
|
||||
const followedFeed =
|
||||
followedCategory === 'all' ? feed : categoryFeeds?.[followedCategory]
|
||||
|
||||
return feed && contracts
|
||||
? feed.map(({ contract, ...other }, i) => ({
|
||||
...other,
|
||||
contract: contracts[i],
|
||||
}))
|
||||
: undefined
|
||||
return followedFeed
|
||||
}
|
||||
|
||||
const getDefaultFeed = async () => {
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { Contract, listenForContract } from 'web/lib/firebase/contracts'
|
||||
import { useEffect } from 'react'
|
||||
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
|
||||
import {
|
||||
Contract,
|
||||
contractDocRef,
|
||||
listenForContract,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { useStateCheckEquality } from './use-state-check-equality'
|
||||
import { DocumentData } from 'firebase/firestore'
|
||||
|
||||
export const useContract = (contractId: string) => {
|
||||
const [contract, setContract] = useState<Contract | null | 'loading'>(
|
||||
'loading'
|
||||
const result = useFirestoreDocumentData<DocumentData, Contract>(
|
||||
['contracts', contractId],
|
||||
contractDocRef(contractId),
|
||||
{ subscribe: true, includeMetadataChanges: true }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (contractId) return listenForContract(contractId, setContract)
|
||||
}, [contractId])
|
||||
|
||||
return contract
|
||||
return result.isLoading ? undefined : result.data
|
||||
}
|
||||
|
||||
export const useContractWithPreload = (initial: Contract | null) => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getValues, listenForValues } from './utils'
|
|||
import { db } from './init'
|
||||
import { User } from 'common/user'
|
||||
import { Comment } from 'common/comment'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
|
||||
export type { Comment }
|
||||
|
||||
|
@ -23,12 +24,13 @@ export async function createComment(
|
|||
text: string,
|
||||
commenter: User,
|
||||
betId?: string,
|
||||
answerOutcome?: string
|
||||
answerOutcome?: string,
|
||||
replyToCommentId?: string
|
||||
) {
|
||||
const ref = betId
|
||||
? doc(getCommentsCollection(contractId), betId)
|
||||
: doc(getCommentsCollection(contractId))
|
||||
const comment: Comment = {
|
||||
const comment: Comment = removeUndefinedProps({
|
||||
id: ref.id,
|
||||
contractId,
|
||||
userId: commenter.id,
|
||||
|
@ -37,13 +39,10 @@ export async function createComment(
|
|||
userName: commenter.name,
|
||||
userUsername: commenter.username,
|
||||
userAvatarUrl: commenter.avatarUrl,
|
||||
}
|
||||
if (betId) {
|
||||
comment.betId = betId
|
||||
}
|
||||
if (answerOutcome) {
|
||||
comment.answerOutcome = answerOutcome
|
||||
}
|
||||
betId: betId,
|
||||
answerOutcome: answerOutcome,
|
||||
replyToCommentId: replyToCommentId,
|
||||
})
|
||||
return await setDoc(ref, comment)
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,8 @@ export function tradingAllowed(contract: Contract) {
|
|||
|
||||
const db = getFirestore(app)
|
||||
export const contractCollection = collection(db, 'contracts')
|
||||
export const contractDocRef = (contractId: string) =>
|
||||
doc(db, 'contracts', contractId)
|
||||
|
||||
// Push contract to Firestore
|
||||
export async function setContract(contract: Contract) {
|
||||
|
|
|
@ -25,9 +25,8 @@ import { PrivateUser, User } from 'common/user'
|
|||
import { createUser } from './api-call'
|
||||
import { getValue, getValues, listenForValue, listenForValues } from './utils'
|
||||
import { DAY_MS } from 'common/util/time'
|
||||
import { Contract } from './contracts'
|
||||
import { Bet } from './bets'
|
||||
import { Comment } from './comments'
|
||||
import { feed } from 'common/feed'
|
||||
import { CATEGORY_LIST } from 'common/categories'
|
||||
|
||||
export type { User }
|
||||
|
||||
|
@ -216,11 +215,18 @@ export async function getDailyNewUsers(
|
|||
export async function getUserFeed(userId: string) {
|
||||
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
|
||||
const userFeed = await getValue<{
|
||||
feed: {
|
||||
contract: Contract
|
||||
recentBets: Bet[]
|
||||
recentComments: Comment[]
|
||||
}[]
|
||||
feed: feed
|
||||
}>(feedDoc)
|
||||
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][])
|
||||
}
|
||||
|
|
|
@ -316,7 +316,6 @@ function ContractTopTrades(props: {
|
|||
contract={contract}
|
||||
comment={commentsById[topCommentId]}
|
||||
betsBySameUser={[betsById[topCommentId]]}
|
||||
hideOutcome={false}
|
||||
truncate={false}
|
||||
smallAvatar={false}
|
||||
/>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useEffect } from 'react'
|
|||
import Head from 'next/head'
|
||||
import Script from 'next/script'
|
||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
|
||||
function firstLine(msg: string) {
|
||||
return msg.replace(/\r?\n.*/s, '')
|
||||
|
@ -77,9 +78,13 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
/>
|
||||
</Head>
|
||||
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Component {...pageProps} />
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export default MyApp
|
||||
|
|
|
@ -13,13 +13,12 @@ import { InfoTooltip } from 'web/components/info-tooltip'
|
|||
import { Page } from 'web/components/page'
|
||||
import { Title } from 'web/components/title'
|
||||
import { ProbabilitySelector } from 'web/components/probability-selector'
|
||||
import { parseWordsAsTags } from 'common/util/parse'
|
||||
import { TagsList } from 'web/components/tags-list'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { MAX_DESCRIPTION_LENGTH, outcomeType } from 'common/contract'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
import { useHasCreatedContractToday } from 'web/hooks/use-has-created-contract-today'
|
||||
import { removeUndefinedProps } from 'common/util/object'
|
||||
import { removeUndefinedProps } from '../../common/util/object'
|
||||
import { CATEGORIES, CATEGORY_LIST, TO_CATEGORY } from 'common/categories'
|
||||
|
||||
export default function Create() {
|
||||
const [question, setQuestion] = useState('')
|
||||
|
@ -70,8 +69,10 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
const [minString, setMinString] = useState('')
|
||||
const [maxString, setMaxString] = 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)
|
||||
|
||||
|
@ -126,7 +127,7 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
initialProb,
|
||||
ante,
|
||||
closeTime,
|
||||
tags,
|
||||
tags: category ? [category] : undefined,
|
||||
min,
|
||||
max,
|
||||
})
|
||||
|
@ -256,25 +257,29 @@ export function NewContract(props: { question: string; tag?: string }) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* <Spacer h={4} />
|
||||
<Spacer h={4} />
|
||||
|
||||
<div className="form-control max-w-sm items-start">
|
||||
<label className="label gap-2">
|
||||
<span className="mb-1">Tags</span>
|
||||
<InfoTooltip text="Optional. Help categorize your market with related tags." />
|
||||
<span className="mb-1">Category</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
placeholder="e.g. Politics, Economics..."
|
||||
className="input input-bordered resize-none"
|
||||
disabled={isSubmitting}
|
||||
value={tagText}
|
||||
onChange={(e) => setTagText(e.target.value || '')}
|
||||
/>
|
||||
</div> */}
|
||||
<select
|
||||
className="select select-bordered w-full max-w-xs"
|
||||
onChange={(e) =>
|
||||
setCategory(TO_CATEGORY[e.currentTarget.value] ?? '')
|
||||
}
|
||||
>
|
||||
<option selected={category === ''}></option>
|
||||
|
||||
{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} />
|
||||
|
||||
<div className="form-control mb-1 items-start">
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useUser } from 'web/hooks/use-user'
|
|||
import { LoadingIndicator } from 'web/components/loading-indicator'
|
||||
import { useAlgoFeed } from 'web/hooks/use-algo-feed'
|
||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
||||
import { CategorySelector } from '../components/feed/category-selector'
|
||||
|
||||
const Home = () => {
|
||||
const user = useUser()
|
||||
|
@ -42,7 +43,12 @@ const Home = () => {
|
|||
<Col className="items-center">
|
||||
<Col className="w-full max-w-[700px]">
|
||||
<FeedCreate user={user ?? undefined} />
|
||||
<Spacer h={10} />
|
||||
<Spacer h={2} />
|
||||
|
||||
<CategorySelector user={user} />
|
||||
|
||||
<Spacer h={1} />
|
||||
|
||||
{feed ? (
|
||||
<ActivityFeed
|
||||
feed={feed}
|
||||
|
|
85
yarn.lock
85
yarn.lock
|
@ -140,7 +140,7 @@
|
|||
core-js-pure "^3.20.2"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||
|
@ -958,6 +958,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||
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":
|
||||
version "9.2.6"
|
||||
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"
|
||||
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:
|
||||
version "9.0.2"
|
||||
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:
|
||||
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:
|
||||
version "4.19.1"
|
||||
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"
|
||||
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:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b"
|
||||
|
@ -3475,6 +3504,11 @@ jose@^2.0.5:
|
|||
dependencies:
|
||||
"@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:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
|
@ -3827,6 +3861,14 @@ make-dir@^3.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
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":
|
||||
version "1.51.0"
|
||||
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"
|
||||
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:
|
||||
version "3.2.0"
|
||||
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"
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
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:
|
||||
version "0.3.4"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
|
@ -5421,6 +5494,14 @@ unique-string@^2.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
|
Loading…
Reference in New Issue
Block a user