Merge branch 'main' into atlas2
This commit is contained in:
commit
f622eaca88
|
@ -1,6 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['lodash'],
|
plugins: ['lodash'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
|
ignorePatterns: ['lib'],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
|
@ -31,6 +32,7 @@ module.exports = {
|
||||||
rules: {
|
rules: {
|
||||||
'no-extra-semi': 'off',
|
'no-extra-semi': 'off',
|
||||||
'no-constant-condition': ['error', { checkLoops: false }],
|
'no-constant-condition': ['error', { checkLoops: false }],
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import { noFees } from './fees'
|
||||||
import { addObjects } from './util/object'
|
import { addObjects } from './util/object'
|
||||||
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
import { NUMERIC_FIXED_VAR } from './numeric-constants'
|
||||||
|
|
||||||
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'>
|
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'>
|
||||||
export type BetInfo = {
|
export type BetInfo = {
|
||||||
newBet: CandidateBet<Bet>
|
newBet: CandidateBet
|
||||||
newPool?: { [outcome: string]: number }
|
newPool?: { [outcome: string]: number }
|
||||||
newTotalShares?: { [outcome: string]: number }
|
newTotalShares?: { [outcome: string]: number }
|
||||||
newTotalBets?: { [outcome: string]: number }
|
newTotalBets?: { [outcome: string]: number }
|
||||||
|
@ -46,7 +46,7 @@ export const getNewBinaryCpmmBetInfo = (
|
||||||
const probBefore = getCpmmProbability(pool, p)
|
const probBefore = getCpmmProbability(pool, p)
|
||||||
const probAfter = getCpmmProbability(newPool, newP)
|
const probAfter = getCpmmProbability(newPool, newP)
|
||||||
|
|
||||||
const newBet: CandidateBet<Bet> = {
|
const newBet: CandidateBet = {
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
shares,
|
shares,
|
||||||
|
@ -96,7 +96,7 @@ export const getNewBinaryDpmBetInfo = (
|
||||||
const probBefore = getDpmProbability(contract.totalShares)
|
const probBefore = getDpmProbability(contract.totalShares)
|
||||||
const probAfter = getDpmProbability(newTotalShares)
|
const probAfter = getDpmProbability(newTotalShares)
|
||||||
|
|
||||||
const newBet: CandidateBet<Bet> = {
|
const newBet: CandidateBet = {
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
loanAmount,
|
loanAmount,
|
||||||
|
@ -133,7 +133,7 @@ export const getNewMultiBetInfo = (
|
||||||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||||
|
|
||||||
const newBet: CandidateBet<Bet> = {
|
const newBet: CandidateBet = {
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount,
|
amount,
|
||||||
loanAmount,
|
loanAmount,
|
||||||
|
|
|
@ -22,6 +22,8 @@ export type Notification = {
|
||||||
|
|
||||||
sourceSlug?: string
|
sourceSlug?: string
|
||||||
sourceTitle?: string
|
sourceTitle?: string
|
||||||
|
|
||||||
|
isSeenOnHref?: string
|
||||||
}
|
}
|
||||||
export type notification_source_types =
|
export type notification_source_types =
|
||||||
| 'contract'
|
| 'contract'
|
||||||
|
@ -34,6 +36,7 @@ export type notification_source_types =
|
||||||
| 'admin_message'
|
| 'admin_message'
|
||||||
| 'group'
|
| 'group'
|
||||||
| 'user'
|
| 'user'
|
||||||
|
| 'bonus'
|
||||||
|
|
||||||
export type notification_source_update_types =
|
export type notification_source_update_types =
|
||||||
| 'created'
|
| 'created'
|
||||||
|
@ -56,3 +59,5 @@ export type notification_reason_types =
|
||||||
| 'added_you_to_group'
|
| 'added_you_to_group'
|
||||||
| 'you_referred_user'
|
| 'you_referred_user'
|
||||||
| 'user_joined_to_bet_on_your_market'
|
| 'user_joined_to_bet_on_your_market'
|
||||||
|
| 'unique_bettors_on_your_contract'
|
||||||
|
| 'on_group_you_are_member_of'
|
||||||
|
|
|
@ -3,3 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005
|
||||||
|
|
||||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||||
|
export const UNIQUE_BETTOR_BONUS_AMOUNT = 5
|
||||||
|
|
54
common/redeem.ts
Normal file
54
common/redeem.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { partition, sumBy } from 'lodash'
|
||||||
|
|
||||||
|
import { Bet } from './bet'
|
||||||
|
import { getProbability } from './calculate'
|
||||||
|
import { CPMMContract } from './contract'
|
||||||
|
import { noFees } from './fees'
|
||||||
|
import { CandidateBet } from './new-bet'
|
||||||
|
|
||||||
|
type RedeemableBet = Pick<Bet, 'outcome' | 'shares' | 'loanAmount'>
|
||||||
|
|
||||||
|
export const getRedeemableAmount = (bets: RedeemableBet[]) => {
|
||||||
|
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
|
||||||
|
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||||
|
const noShares = sumBy(noBets, (b) => b.shares)
|
||||||
|
const shares = Math.max(Math.min(yesShares, noShares), 0)
|
||||||
|
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
||||||
|
const loanPayment = Math.min(loanAmount, shares)
|
||||||
|
const netAmount = shares - loanPayment
|
||||||
|
return { shares, loanPayment, netAmount }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRedemptionBets = (
|
||||||
|
shares: number,
|
||||||
|
loanPayment: number,
|
||||||
|
contract: CPMMContract
|
||||||
|
) => {
|
||||||
|
const p = getProbability(contract)
|
||||||
|
const createdTime = Date.now()
|
||||||
|
const yesBet: CandidateBet = {
|
||||||
|
contractId: contract.id,
|
||||||
|
amount: p * -shares,
|
||||||
|
shares: -shares,
|
||||||
|
loanAmount: loanPayment ? -loanPayment / 2 : 0,
|
||||||
|
outcome: 'YES',
|
||||||
|
probBefore: p,
|
||||||
|
probAfter: p,
|
||||||
|
createdTime,
|
||||||
|
isRedemption: true,
|
||||||
|
fees: noFees,
|
||||||
|
}
|
||||||
|
const noBet: CandidateBet = {
|
||||||
|
contractId: contract.id,
|
||||||
|
amount: (1 - p) * -shares,
|
||||||
|
shares: -shares,
|
||||||
|
loanAmount: loanPayment ? -loanPayment / 2 : 0,
|
||||||
|
outcome: 'NO',
|
||||||
|
probBefore: p,
|
||||||
|
probAfter: p,
|
||||||
|
createdTime,
|
||||||
|
isRedemption: true,
|
||||||
|
fees: noFees,
|
||||||
|
}
|
||||||
|
return [yesBet, noBet]
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||||
type AnyTxnType = Donation | Tip | Manalink | Referral
|
type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus
|
||||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||||
|
|
||||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
|
@ -16,7 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
amount: number
|
amount: number
|
||||||
token: 'M$' // | 'USD' | MarketOutcome
|
token: 'M$' // | 'USD' | MarketOutcome
|
||||||
|
|
||||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||||
|
|
||||||
// Any extra data
|
// Any extra data
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
|
||||||
|
@ -52,6 +53,12 @@ type Referral = {
|
||||||
category: 'REFERRAL'
|
category: 'REFERRAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Bonus = {
|
||||||
|
fromType: 'BANK'
|
||||||
|
toType: 'USER'
|
||||||
|
category: 'UNIQUE_BETTOR_BONUS'
|
||||||
|
}
|
||||||
|
|
||||||
export type DonationTxn = Txn & Donation
|
export type DonationTxn = Txn & Donation
|
||||||
export type TipTxn = Txn & Tip
|
export type TipTxn = Txn & Tip
|
||||||
export type ManalinkTxn = Txn & Manalink
|
export type ManalinkTxn = Txn & Manalink
|
||||||
|
|
|
@ -57,6 +57,7 @@ export type PrivateUser = {
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
notificationPreferences?: notification_subscribe_types
|
notificationPreferences?: notification_subscribe_types
|
||||||
|
lastTimeCheckedBonuses?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||||
|
|
|
@ -337,6 +337,20 @@
|
||||||
"order": "DESCENDING"
|
"order": "DESCENDING"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collectionGroup": "portfolioHistory",
|
||||||
|
"queryScope": "COLLECTION",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"order": "ASCENDING"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timestamp",
|
||||||
|
"order": "ASCENDING"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fieldOverrides": [
|
"fieldOverrides": [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ['lodash'],
|
plugins: ['lodash'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
ignorePatterns: ['lib'],
|
ignorePatterns: ['dist', 'lib'],
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
},
|
},
|
||||||
|
@ -30,6 +30,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"main": "functions/src/index.js",
|
"main": "functions/src/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@amplitude/node": "1.10.0",
|
"@amplitude/node": "1.10.0",
|
||||||
|
"@google-cloud/functions-framework": "3.1.2",
|
||||||
"firebase-admin": "10.0.0",
|
"firebase-admin": "10.0.0",
|
||||||
"firebase-functions": "3.21.2",
|
"firebase-functions": "3.21.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
type user_to_reason_texts = {
|
type user_to_reason_texts = {
|
||||||
[userId: string]: { reason: notification_reason_types }
|
[userId: string]: { reason: notification_reason_types; isSeeOnHref?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNotification = async (
|
export const createNotification = async (
|
||||||
|
@ -72,6 +72,7 @@ export const createNotification = async (
|
||||||
sourceContractSlug: sourceContract?.slug,
|
sourceContractSlug: sourceContract?.slug,
|
||||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
||||||
|
isSeenOnHref: userToReasonTexts[userId].isSeeOnHref,
|
||||||
}
|
}
|
||||||
await notificationRef.set(removeUndefinedProps(notification))
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
})
|
})
|
||||||
|
@ -267,6 +268,26 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyContractCreatorOfUniqueBettorsBonus = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'unique_bettors_on_your_contract',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifyOtherGroupMembersOfComment = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
|
if (shouldGetNotification(userId, userToReasonTexts))
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_group_you_are_member_of',
|
||||||
|
isSeeOnHref: sourceSlug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getUsersToNotify = async () => {
|
const getUsersToNotify = async () => {
|
||||||
const userToReasonTexts: user_to_reason_texts = {}
|
const userToReasonTexts: user_to_reason_texts = {}
|
||||||
// The following functions modify the userToReasonTexts object in place.
|
// The following functions modify the userToReasonTexts object in place.
|
||||||
|
@ -277,6 +298,8 @@ export const createNotification = async (
|
||||||
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
||||||
} else if (sourceType === 'user' && relatedUserId) {
|
} else if (sourceType === 'user' && relatedUserId) {
|
||||||
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
|
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
|
||||||
|
} else if (sourceType === 'comment' && !sourceContract && relatedUserId) {
|
||||||
|
await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following functions need sourceContract to be defined.
|
// The following functions need sourceContract to be defined.
|
||||||
|
@ -309,6 +332,12 @@ export const createNotification = async (
|
||||||
})
|
})
|
||||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||||
|
} else if (sourceType === 'bonus' && sourceUpdateType === 'created') {
|
||||||
|
// Note: the daily bonus won't have a contract attached to it
|
||||||
|
await notifyContractCreatorOfUniqueBettorsBonus(
|
||||||
|
userToReasonTexts,
|
||||||
|
sourceContract.creatorId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return userToReasonTexts
|
return userToReasonTexts
|
||||||
}
|
}
|
||||||
|
|
139
functions/src/get-daily-bonuses.ts
Normal file
139
functions/src/get-daily-bonuses.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { APIError, newEndpoint } from './api'
|
||||||
|
import { log } from './utils'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { PrivateUser } from '../../common/lib/user'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import { Bet } from '../../common/lib/bet'
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||||
|
import { runTxn, TxnData } from './transact'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
import { User } from '../../common/lib/user'
|
||||||
|
import { Contract } from '../../common/lib/contract'
|
||||||
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
||||||
|
|
||||||
|
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
|
||||||
|
const QUERY_LIMIT_SECONDS = 60
|
||||||
|
|
||||||
|
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
|
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
|
||||||
|
async (trans) => {
|
||||||
|
const userSnap = await trans.get(
|
||||||
|
firestore.doc(`private-users/${auth.uid}`)
|
||||||
|
)
|
||||||
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
|
const user = userSnap.data() as PrivateUser
|
||||||
|
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
|
||||||
|
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
|
||||||
|
throw new APIError(
|
||||||
|
400,
|
||||||
|
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
|
||||||
|
)
|
||||||
|
await trans.update(userSnap.ref, {
|
||||||
|
lastTimeCheckedBonuses: Date.now(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
lastTimeCheckedBonuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// TODO: switch to prod id
|
||||||
|
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
|
||||||
|
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
|
||||||
|
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||||
|
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||||
|
const fromUser = fromSnap.data() as User
|
||||||
|
// Get all users contracts made since implementation time
|
||||||
|
const userContractsSnap = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.where('creatorId', '==', user.id)
|
||||||
|
.where('createdTime', '>=', BONUS_START_DATE)
|
||||||
|
.get()
|
||||||
|
const userContracts = userContractsSnap.docs.map(
|
||||||
|
(doc) => doc.data() as Contract
|
||||||
|
)
|
||||||
|
const nullReturn = { status: 'no bets', txn: null }
|
||||||
|
for (const contract of userContracts) {
|
||||||
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
|
const contractId = contract.id
|
||||||
|
// Get all bets made on user's contracts
|
||||||
|
const bets = (
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.where('userId', '!=', user.id)
|
||||||
|
.get()
|
||||||
|
).docs.map((bet) => bet.ref)
|
||||||
|
if (bets.length === 0) {
|
||||||
|
return nullReturn
|
||||||
|
}
|
||||||
|
const contractBetsSnap = await trans.getAll(...bets)
|
||||||
|
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
|
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
||||||
|
contractBets
|
||||||
|
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
|
||||||
|
.map((bet) => bet.userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter users for ONLY those that have made bets since the last daily bonus received time
|
||||||
|
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
|
||||||
|
contractBets
|
||||||
|
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
|
||||||
|
.map((bet) => bet.userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter for users only present in the above list
|
||||||
|
const newUniqueBettorIds =
|
||||||
|
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
||||||
|
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
||||||
|
)
|
||||||
|
newUniqueBettorIds.length > 0 &&
|
||||||
|
log(
|
||||||
|
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
||||||
|
)
|
||||||
|
if (newUniqueBettorIds.length === 0) {
|
||||||
|
return nullReturn
|
||||||
|
}
|
||||||
|
// Create combined txn for all unique bettors
|
||||||
|
const bonusTxnDetails = {
|
||||||
|
contractId: contractId,
|
||||||
|
uniqueBettors: newUniqueBettorIds.length,
|
||||||
|
}
|
||||||
|
const bonusTxn: TxnData = {
|
||||||
|
fromId: fromUser.id,
|
||||||
|
fromType: 'BANK',
|
||||||
|
toId: user.id,
|
||||||
|
toType: 'USER',
|
||||||
|
amount: UNIQUE_BETTOR_BONUS_AMOUNT * newUniqueBettorIds.length,
|
||||||
|
token: 'M$',
|
||||||
|
category: 'UNIQUE_BETTOR_BONUS',
|
||||||
|
description: JSON.stringify(bonusTxnDetails),
|
||||||
|
}
|
||||||
|
return await runTxn(trans, bonusTxn)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.status != 'success' || !result.txn) {
|
||||||
|
result.status != nullReturn.status &&
|
||||||
|
log(`No bonus for user: ${user.id} - reason:`, result.status)
|
||||||
|
} else {
|
||||||
|
log(`Bonus txn for user: ${user.id} completed:`, result.txn?.id)
|
||||||
|
await createNotification(
|
||||||
|
result.txn.id,
|
||||||
|
'bonus',
|
||||||
|
'created',
|
||||||
|
fromUser,
|
||||||
|
result.txn.id,
|
||||||
|
result.txn.amount + '',
|
||||||
|
contract,
|
||||||
|
undefined,
|
||||||
|
// No need to set the user id, we'll use the contract creator id
|
||||||
|
undefined,
|
||||||
|
contract.slug,
|
||||||
|
contract.question
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { userId: user.id, message: 'success' }
|
||||||
|
})
|
|
@ -10,7 +10,7 @@ export * from './stripe'
|
||||||
export * from './create-user'
|
export * from './create-user'
|
||||||
export * from './create-answer'
|
export * from './create-answer'
|
||||||
export * from './on-create-bet'
|
export * from './on-create-bet'
|
||||||
export * from './on-create-comment'
|
export * from './on-create-comment-on-contract'
|
||||||
export * from './on-view'
|
export * from './on-view'
|
||||||
export * from './unsubscribe'
|
export * from './unsubscribe'
|
||||||
export * from './update-metrics'
|
export * from './update-metrics'
|
||||||
|
@ -28,6 +28,7 @@ export * from './on-create-liquidity-provision'
|
||||||
export * from './on-update-group'
|
export * from './on-update-group'
|
||||||
export * from './on-create-group'
|
export * from './on-create-group'
|
||||||
export * from './on-update-user'
|
export * from './on-update-user'
|
||||||
|
export * from './on-create-comment-on-group'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
@ -38,3 +39,4 @@ export * from './create-contract'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
export * from './create-group'
|
export * from './create-group'
|
||||||
export * from './resolve-market'
|
export * from './resolve-market'
|
||||||
|
export * from './get-daily-bonuses'
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { createNotification } from './create-notification'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
export const onCreateComment = functions
|
export const onCreateCommentOnContract = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
.firestore.document('contracts/{contractId}/comments/{commentId}')
|
.firestore.document('contracts/{contractId}/comments/{commentId}')
|
||||||
.onCreate(async (change, context) => {
|
.onCreate(async (change, context) => {
|
52
functions/src/on-create-comment-on-group.ts
Normal file
52
functions/src/on-create-comment-on-group.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import { Comment } from '../../common/comment'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { Group } from '../../common/group'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const onCreateCommentOnGroup = functions.firestore
|
||||||
|
.document('groups/{groupId}/comments/{commentId}')
|
||||||
|
.onCreate(async (change, context) => {
|
||||||
|
const { eventId } = context
|
||||||
|
const { groupId } = context.params as {
|
||||||
|
groupId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = change.data() as Comment
|
||||||
|
const creatorSnapshot = await firestore
|
||||||
|
.collection('users')
|
||||||
|
.doc(comment.userId)
|
||||||
|
.get()
|
||||||
|
if (!creatorSnapshot.exists) throw new Error('Could not find user')
|
||||||
|
|
||||||
|
const groupSnapshot = await firestore
|
||||||
|
.collection('groups')
|
||||||
|
.doc(groupId)
|
||||||
|
.get()
|
||||||
|
if (!groupSnapshot.exists) throw new Error('Could not find group')
|
||||||
|
|
||||||
|
const group = groupSnapshot.data() as Group
|
||||||
|
await firestore.collection('groups').doc(groupId).update({
|
||||||
|
mostRecentActivityTime: comment.createdTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
group.memberIds.map(async (memberId) => {
|
||||||
|
return await createNotification(
|
||||||
|
comment.id,
|
||||||
|
'comment',
|
||||||
|
'created',
|
||||||
|
creatorSnapshot.data() as User,
|
||||||
|
eventId,
|
||||||
|
comment.text,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
memberId,
|
||||||
|
`/group/${group.slug}`,
|
||||||
|
`${group.name}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
|
@ -12,6 +12,7 @@ export const onUpdateGroup = functions.firestore
|
||||||
// ignore the update we just made
|
// ignore the update we just made
|
||||||
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
|
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
|
||||||
return
|
return
|
||||||
|
// TODO: create notification with isSeeOnHref set to the group's /group/questions url
|
||||||
|
|
||||||
await firestore
|
await firestore
|
||||||
.collection('groups')
|
.collection('groups')
|
||||||
|
|
|
@ -1,96 +1,46 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { partition, sumBy } from 'lodash'
|
|
||||||
|
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getRedeemableAmount, getRedemptionBets } from '../../common/redeem'
|
||||||
|
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { noFees } from '../../common/fees'
|
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
|
|
||||||
export const redeemShares = async (userId: string, contractId: string) => {
|
export const redeemShares = async (userId: string, contractId: string) => {
|
||||||
return await firestore.runTransaction(async (transaction) => {
|
return await firestore.runTransaction(async (trans) => {
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await transaction.get(contractDoc)
|
const contractSnap = await trans.get(contractDoc)
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
return { status: 'error', message: 'Invalid contract' }
|
return { status: 'error', message: 'Invalid contract' }
|
||||||
|
|
||||||
const contract = contractSnap.data() as Contract
|
const contract = contractSnap.data() as Contract
|
||||||
const { mechanism, outcomeType } = contract
|
const { mechanism } = contract
|
||||||
if (
|
if (mechanism !== 'cpmm-1') return { status: 'success' }
|
||||||
!(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') ||
|
|
||||||
mechanism !== 'cpmm-1'
|
|
||||||
)
|
|
||||||
return { status: 'success' }
|
|
||||||
|
|
||||||
const betsSnap = await transaction.get(
|
const betsColl = firestore.collection(`contracts/${contract.id}/bets`)
|
||||||
firestore
|
const betsSnap = await trans.get(betsColl.where('userId', '==', userId))
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
|
||||||
.where('userId', '==', userId)
|
|
||||||
)
|
|
||||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||||
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
|
const { shares, loanPayment, netAmount } = getRedeemableAmount(bets)
|
||||||
const yesShares = sumBy(yesBets, (b) => b.shares)
|
if (netAmount === 0) {
|
||||||
const noShares = sumBy(noBets, (b) => b.shares)
|
return { status: 'success' }
|
||||||
|
|
||||||
const amount = Math.min(yesShares, noShares)
|
|
||||||
if (amount <= 0) return
|
|
||||||
|
|
||||||
const prevLoanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
|
|
||||||
const loanPaid = Math.min(prevLoanAmount, amount)
|
|
||||||
const netAmount = amount - loanPaid
|
|
||||||
|
|
||||||
const p = getProbability(contract)
|
|
||||||
const createdTime = Date.now()
|
|
||||||
|
|
||||||
const yesDoc = firestore.collection(`contracts/${contract.id}/bets`).doc()
|
|
||||||
const yesBet: Bet = {
|
|
||||||
id: yesDoc.id,
|
|
||||||
userId: userId,
|
|
||||||
contractId: contract.id,
|
|
||||||
amount: p * -amount,
|
|
||||||
shares: -amount,
|
|
||||||
loanAmount: loanPaid ? -loanPaid / 2 : 0,
|
|
||||||
outcome: 'YES',
|
|
||||||
probBefore: p,
|
|
||||||
probAfter: p,
|
|
||||||
createdTime,
|
|
||||||
isRedemption: true,
|
|
||||||
fees: noFees,
|
|
||||||
}
|
|
||||||
|
|
||||||
const noDoc = firestore.collection(`contracts/${contract.id}/bets`).doc()
|
|
||||||
const noBet: Bet = {
|
|
||||||
id: noDoc.id,
|
|
||||||
userId: userId,
|
|
||||||
contractId: contract.id,
|
|
||||||
amount: (1 - p) * -amount,
|
|
||||||
shares: -amount,
|
|
||||||
loanAmount: loanPaid ? -loanPaid / 2 : 0,
|
|
||||||
outcome: 'NO',
|
|
||||||
probBefore: p,
|
|
||||||
probAfter: p,
|
|
||||||
createdTime,
|
|
||||||
isRedemption: true,
|
|
||||||
fees: noFees,
|
|
||||||
}
|
}
|
||||||
|
const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract)
|
||||||
|
|
||||||
const userDoc = firestore.doc(`users/${userId}`)
|
const userDoc = firestore.doc(`users/${userId}`)
|
||||||
const userSnap = await transaction.get(userDoc)
|
const userSnap = await trans.get(userDoc)
|
||||||
if (!userSnap.exists) return { status: 'error', message: 'User not found' }
|
if (!userSnap.exists) return { status: 'error', message: 'User not found' }
|
||||||
|
|
||||||
const user = userSnap.data() as User
|
const user = userSnap.data() as User
|
||||||
|
|
||||||
const newBalance = user.balance + netAmount
|
const newBalance = user.balance + netAmount
|
||||||
|
|
||||||
if (!isFinite(newBalance)) {
|
if (!isFinite(newBalance)) {
|
||||||
throw new Error('Invalid user balance for ' + user.username)
|
throw new Error('Invalid user balance for ' + user.username)
|
||||||
}
|
}
|
||||||
|
|
||||||
transaction.update(userDoc, { balance: newBalance })
|
const yesDoc = betsColl.doc()
|
||||||
|
const noDoc = betsColl.doc()
|
||||||
transaction.create(yesDoc, yesBet)
|
trans.update(userDoc, { balance: newBalance })
|
||||||
transaction.create(noDoc, noBet)
|
trans.create(yesDoc, { id: yesDoc.id, userId, ...yesBet })
|
||||||
|
trans.create(noDoc, { id: noDoc.id, userId, ...noBet })
|
||||||
|
|
||||||
return { status: 'success' }
|
return { status: 'success' }
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
||||||
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||||
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
const maxShares = sumBy(outcomeBets, (bet) => bet.shares)
|
||||||
|
|
||||||
if (shares > maxShares + 0.000000000001)
|
if (shares > maxShares)
|
||||||
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
throw new APIError(400, `You can only sell up to ${maxShares} shares.`)
|
||||||
|
|
||||||
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
||||||
|
|
|
@ -1,138 +1,138 @@
|
||||||
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 { CPMMContract } from '../../common/contract'
|
import { CPMMContract } from '../../common/contract'
|
||||||
import { User } from '../../common/user'
|
import { User } from '../../common/user'
|
||||||
import { subtractObjects } from '../../common/util/object'
|
import { subtractObjects } from '../../common/util/object'
|
||||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||||
import { getUserLiquidityShares } from '../../common/calculate-cpmm'
|
import { getUserLiquidityShares } from '../../common/calculate-cpmm'
|
||||||
import { Bet } from '../../common/bet'
|
import { Bet } from '../../common/bet'
|
||||||
import { getProbability } from '../../common/calculate'
|
import { getProbability } from '../../common/calculate'
|
||||||
import { noFees } from '../../common/fees'
|
import { noFees } from '../../common/fees'
|
||||||
|
|
||||||
import { APIError } from './api'
|
import { APIError } from './api'
|
||||||
import { redeemShares } from './redeem-shares'
|
import { redeemShares } from './redeem-shares'
|
||||||
|
|
||||||
export const withdrawLiquidity = functions
|
export const withdrawLiquidity = functions
|
||||||
.runWith({ minInstances: 1 })
|
.runWith({ minInstances: 1 })
|
||||||
.https.onCall(
|
.https.onCall(
|
||||||
async (
|
async (
|
||||||
data: {
|
data: {
|
||||||
contractId: string
|
contractId: string
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const userId = context?.auth?.uid
|
const userId = context?.auth?.uid
|
||||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||||
|
|
||||||
const { contractId } = data
|
const { contractId } = data
|
||||||
if (!contractId)
|
if (!contractId)
|
||||||
return { status: 'error', message: 'Missing contract id' }
|
return { status: 'error', message: 'Missing contract id' }
|
||||||
|
|
||||||
return await firestore
|
return await firestore
|
||||||
.runTransaction(async (trans) => {
|
.runTransaction(async (trans) => {
|
||||||
const lpDoc = firestore.doc(`users/${userId}`)
|
const lpDoc = firestore.doc(`users/${userId}`)
|
||||||
const lpSnap = await trans.get(lpDoc)
|
const lpSnap = await trans.get(lpDoc)
|
||||||
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
|
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
const lp = lpSnap.data() as User
|
const lp = lpSnap.data() as User
|
||||||
|
|
||||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||||
const contractSnap = await trans.get(contractDoc)
|
const contractSnap = await trans.get(contractDoc)
|
||||||
if (!contractSnap.exists)
|
if (!contractSnap.exists)
|
||||||
throw new APIError(400, 'Contract not found.')
|
throw new APIError(400, 'Contract not found.')
|
||||||
const contract = contractSnap.data() as CPMMContract
|
const contract = contractSnap.data() as CPMMContract
|
||||||
|
|
||||||
const liquidityCollection = firestore.collection(
|
const liquidityCollection = firestore.collection(
|
||||||
`contracts/${contractId}/liquidity`
|
`contracts/${contractId}/liquidity`
|
||||||
)
|
)
|
||||||
|
|
||||||
const liquiditiesSnap = await trans.get(liquidityCollection)
|
const liquiditiesSnap = await trans.get(liquidityCollection)
|
||||||
|
|
||||||
const liquidities = liquiditiesSnap.docs.map(
|
const liquidities = liquiditiesSnap.docs.map(
|
||||||
(doc) => doc.data() as LiquidityProvision
|
(doc) => doc.data() as LiquidityProvision
|
||||||
)
|
)
|
||||||
|
|
||||||
const userShares = getUserLiquidityShares(
|
const userShares = getUserLiquidityShares(
|
||||||
userId,
|
userId,
|
||||||
contract,
|
contract,
|
||||||
liquidities
|
liquidities
|
||||||
)
|
)
|
||||||
|
|
||||||
// zero all added amounts for now
|
// zero all added amounts for now
|
||||||
// can add support for partial withdrawals in the future
|
// can add support for partial withdrawals in the future
|
||||||
liquiditiesSnap.docs
|
liquiditiesSnap.docs
|
||||||
.filter(
|
.filter(
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
!liquidities[i].isAnte && liquidities[i].userId === userId
|
!liquidities[i].isAnte && liquidities[i].userId === userId
|
||||||
)
|
)
|
||||||
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
|
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
|
||||||
|
|
||||||
const payout = Math.min(...Object.values(userShares))
|
const payout = Math.min(...Object.values(userShares))
|
||||||
if (payout <= 0) return {}
|
if (payout <= 0) return {}
|
||||||
|
|
||||||
const newBalance = lp.balance + payout
|
const newBalance = lp.balance + payout
|
||||||
const newTotalDeposits = lp.totalDeposits + payout
|
const newTotalDeposits = lp.totalDeposits + payout
|
||||||
trans.update(lpDoc, {
|
trans.update(lpDoc, {
|
||||||
balance: newBalance,
|
balance: newBalance,
|
||||||
totalDeposits: newTotalDeposits,
|
totalDeposits: newTotalDeposits,
|
||||||
} as Partial<User>)
|
} as Partial<User>)
|
||||||
|
|
||||||
const newPool = subtractObjects(contract.pool, userShares)
|
const newPool = subtractObjects(contract.pool, userShares)
|
||||||
|
|
||||||
const minPoolShares = Math.min(...Object.values(newPool))
|
const minPoolShares = Math.min(...Object.values(newPool))
|
||||||
const adjustedTotal = contract.totalLiquidity - payout
|
const adjustedTotal = contract.totalLiquidity - payout
|
||||||
|
|
||||||
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
|
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
|
||||||
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
|
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
|
||||||
|
|
||||||
trans.update(contractDoc, {
|
trans.update(contractDoc, {
|
||||||
pool: newPool,
|
pool: newPool,
|
||||||
totalLiquidity: newTotalLiquidity,
|
totalLiquidity: newTotalLiquidity,
|
||||||
})
|
})
|
||||||
|
|
||||||
const prob = getProbability(contract)
|
const prob = getProbability(contract)
|
||||||
|
|
||||||
// surplus shares become user's bets
|
// surplus shares become user's bets
|
||||||
const bets = Object.entries(userShares)
|
const bets = Object.entries(userShares)
|
||||||
.map(([outcome, shares]) =>
|
.map(([outcome, shares]) =>
|
||||||
shares - payout < 1 // don't create bet if less than 1 share
|
shares - payout < 1 // don't create bet if less than 1 share
|
||||||
? undefined
|
? undefined
|
||||||
: ({
|
: ({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
contractId: contract.id,
|
contractId: contract.id,
|
||||||
amount:
|
amount:
|
||||||
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
|
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
|
||||||
shares: shares - payout,
|
shares: shares - payout,
|
||||||
outcome,
|
outcome,
|
||||||
probBefore: prob,
|
probBefore: prob,
|
||||||
probAfter: prob,
|
probAfter: prob,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
isLiquidityProvision: true,
|
isLiquidityProvision: true,
|
||||||
fees: noFees,
|
fees: noFees,
|
||||||
} as Omit<Bet, 'id'>)
|
} as Omit<Bet, 'id'>)
|
||||||
)
|
)
|
||||||
.filter((x) => x !== undefined)
|
.filter((x) => x !== undefined)
|
||||||
|
|
||||||
for (const bet of bets) {
|
for (const bet of bets) {
|
||||||
const doc = firestore
|
const doc = firestore
|
||||||
.collection(`contracts/${contract.id}/bets`)
|
.collection(`contracts/${contract.id}/bets`)
|
||||||
.doc()
|
.doc()
|
||||||
trans.create(doc, { id: doc.id, ...bet })
|
trans.create(doc, { id: doc.id, ...bet })
|
||||||
}
|
}
|
||||||
|
|
||||||
return userShares
|
return userShares
|
||||||
})
|
})
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
// redeem surplus bet with pre-existing bets
|
// redeem surplus bet with pre-existing bets
|
||||||
await redeemShares(userId, contractId)
|
await redeemShares(userId, contractId)
|
||||||
|
|
||||||
console.log('userid', userId, 'withdraws', result)
|
console.log('userid', userId, 'withdraws', result)
|
||||||
return { status: 'success', userShares: result }
|
return { status: 'success', userShares: result }
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
return { status: 'error', message: e.message }
|
return { status: 'error', message: e.message }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
|
@ -19,6 +19,7 @@ module.exports = {
|
||||||
],
|
],
|
||||||
'@next/next/no-img-element': 'off',
|
'@next/next/no-img-element': 'off',
|
||||||
'@next/next/no-typos': 'off',
|
'@next/next/no-typos': 'off',
|
||||||
|
'linebreak-style': ['error', 'unix'],
|
||||||
'lodash/import-scope': [2, 'member'],
|
'lodash/import-scope': [2, 'member'],
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { Title } from '../title'
|
||||||
import { TweetButton } from '../tweet-button'
|
import { TweetButton } from '../tweet-button'
|
||||||
import { InfoTooltip } from '../info-tooltip'
|
import { InfoTooltip } from '../info-tooltip'
|
||||||
import { TagsInput } from 'web/components/tags-input'
|
import { TagsInput } from 'web/components/tags-input'
|
||||||
|
import { DuplicateContractButton } from '../copy-contract-button'
|
||||||
|
|
||||||
export const contractDetailsButtonClassName =
|
export const contractDetailsButtonClassName =
|
||||||
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500'
|
||||||
|
@ -71,6 +72,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
tweetText={getTweetText(contract, false)}
|
tweetText={getTweetText(contract, false)}
|
||||||
/>
|
/>
|
||||||
<ShareEmbedButton contract={contract} toastClassName={'-left-20'} />
|
<ShareEmbedButton contract={contract} toastClassName={'-left-20'} />
|
||||||
|
<DuplicateContractButton contract={contract} />
|
||||||
</Row>
|
</Row>
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
|
|
54
web/components/copy-contract-button.tsx
Normal file
54
web/components/copy-contract-button.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { DuplicateIcon } from '@heroicons/react/outline'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
|
|
||||||
|
export function DuplicateContractButton(props: {
|
||||||
|
contract: Contract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={clsx('btn btn-xs flex-nowrap normal-case', className)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
border: '2px solid #a78bfa',
|
||||||
|
// violet-400
|
||||||
|
color: '#a78bfa',
|
||||||
|
}}
|
||||||
|
href={duplicateContractHref(contract)}
|
||||||
|
onClick={trackCallback('duplicate market')}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<DuplicateIcon className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
||||||
|
<div>Duplicate</div>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass along the Uri to create a new contract
|
||||||
|
function duplicateContractHref(contract: Contract) {
|
||||||
|
const params = {
|
||||||
|
q: contract.question,
|
||||||
|
closeTime: contract.closeTime || 0,
|
||||||
|
description: contract.description,
|
||||||
|
outcomeType: contract.outcomeType,
|
||||||
|
} as Record<string, any>
|
||||||
|
|
||||||
|
if (contract.outcomeType === 'PSEUDO_NUMERIC') {
|
||||||
|
params.min = contract.min
|
||||||
|
params.max = contract.max
|
||||||
|
params.isLogScale = contract.isLogScale
|
||||||
|
params.initValue = getMappedValue(contract)(contract.initialProbability)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
`/create?` +
|
||||||
|
Object.entries(params)
|
||||||
|
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||||
|
.join('&')
|
||||||
|
)
|
||||||
|
}
|
|
@ -35,7 +35,8 @@ export function FilterSelectUsers(props: {
|
||||||
return (
|
return (
|
||||||
!selectedUsers.map((user) => user.name).includes(user.name) &&
|
!selectedUsers.map((user) => user.name).includes(user.name) &&
|
||||||
!ignoreUserIds.includes(user.id) &&
|
!ignoreUserIds.includes(user.id) &&
|
||||||
user.name.toLowerCase().includes(query.toLowerCase())
|
(user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
user.username.toLowerCase().includes(query.toLowerCase()))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo'
|
||||||
import { MenuButton } from './menu'
|
import { MenuButton } from './menu'
|
||||||
import { ProfileSummary } from './profile-menu'
|
import { ProfileSummary } from './profile-menu'
|
||||||
import NotificationsIcon from 'web/components/notifications-icon'
|
import NotificationsIcon from 'web/components/notifications-icon'
|
||||||
import React from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { CreateQuestionButton } from 'web/components/create-question-button'
|
import { CreateQuestionButton } from 'web/components/create-question-button'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
|
@ -26,6 +26,8 @@ import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { trackCallback, withTracking } from 'web/lib/service/analytics'
|
import { trackCallback, withTracking } from 'web/lib/service/analytics'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { Spacer } from '../layout/spacer'
|
import { Spacer } from '../layout/spacer'
|
||||||
|
import { usePreferredNotifications } from 'web/hooks/use-notifications'
|
||||||
|
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
||||||
|
|
||||||
function getNavigation() {
|
function getNavigation() {
|
||||||
return [
|
return [
|
||||||
|
@ -218,7 +220,11 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<GroupsList currentPage={currentPage} memberItems={memberItems} />
|
<GroupsList
|
||||||
|
currentPage={router.asPath}
|
||||||
|
memberItems={memberItems}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop navigation */}
|
{/* Desktop navigation */}
|
||||||
|
@ -237,14 +243,36 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
<div className="h-[1px] bg-gray-300" />
|
<div className="h-[1px] bg-gray-300" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<GroupsList currentPage={currentPage} memberItems={memberItems} />
|
<GroupsList
|
||||||
|
currentPage={router.asPath}
|
||||||
|
memberItems={memberItems}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
function GroupsList(props: {
|
||||||
const { currentPage, memberItems } = props
|
currentPage: string
|
||||||
|
memberItems: Item[]
|
||||||
|
user: User | null | undefined
|
||||||
|
}) {
|
||||||
|
const { currentPage, memberItems, user } = props
|
||||||
|
const preferredNotifications = usePreferredNotifications(user?.id, {
|
||||||
|
unseenOnly: true,
|
||||||
|
customHref: '/group/',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set notification as seen if our current page is equal to the isSeenOnHref property
|
||||||
|
useEffect(() => {
|
||||||
|
preferredNotifications.forEach((notification) => {
|
||||||
|
if (notification.isSeenOnHref === currentPage) {
|
||||||
|
setNotificationsAsSeen([notification])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [currentPage, preferredNotifications])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
|
@ -257,9 +285,14 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
||||||
<a
|
<a
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
className={clsx(
|
||||||
|
'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900',
|
||||||
|
preferredNotifications.some(
|
||||||
|
(n) => !n.isSeen && n.isSeenOnHref === item.href
|
||||||
|
) && 'font-bold'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate"> {item.name}</span>
|
<span className="truncate">{item.name}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,17 +2,29 @@ import { BellIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications'
|
import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications'
|
||||||
|
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
||||||
|
import { requestBonuses } from 'web/lib/firebase/api-call'
|
||||||
|
|
||||||
export default function NotificationsIcon(props: { className?: string }) {
|
export default function NotificationsIcon(props: { className?: string }) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const notifications = usePreferredGroupedNotifications(user?.id, {
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
const notifications = usePreferredGroupedNotifications(privateUser?.id, {
|
||||||
unseenOnly: true,
|
unseenOnly: true,
|
||||||
})
|
})
|
||||||
const [seen, setSeen] = useState(false)
|
const [seen, setSeen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!privateUser) return
|
||||||
|
|
||||||
|
if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 60 * 1000)
|
||||||
|
requestBonuses({}).catch((error) => {
|
||||||
|
console.log("couldn't get bonuses:", error.message)
|
||||||
|
})
|
||||||
|
}, [privateUser])
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.pathname.endsWith('notifications')) return setSeen(true)
|
if (router.pathname.endsWith('notifications')) return setSeen(true)
|
||||||
|
@ -24,7 +36,9 @@ export default function NotificationsIcon(props: { className?: string }) {
|
||||||
<div className={'relative'}>
|
<div className={'relative'}>
|
||||||
{!seen && notifications && notifications.length > 0 && (
|
{!seen && notifications && notifications.length > 0 && (
|
||||||
<div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2">
|
<div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2">
|
||||||
{notifications.length}
|
{notifications.length > NOTIFICATIONS_PER_PAGE
|
||||||
|
? `${NOTIFICATIONS_PER_PAGE}+`
|
||||||
|
: notifications.length}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<BellIcon className={clsx(props.className)} />
|
<BellIcon className={clsx(props.className)} />
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { groupBy, map } from 'lodash'
|
||||||
|
|
||||||
export type NotificationGroup = {
|
export type NotificationGroup = {
|
||||||
notifications: Notification[]
|
notifications: Notification[]
|
||||||
sourceContractId: string
|
groupedById: string
|
||||||
isSeen: boolean
|
isSeen: boolean
|
||||||
timePeriod: string
|
timePeriod: string
|
||||||
|
type: 'income' | 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePreferredGroupedNotifications(
|
export function usePreferredGroupedNotifications(
|
||||||
|
@ -37,25 +38,43 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
new Date(notification.createdTime).toDateString()
|
new Date(notification.createdTime).toDateString()
|
||||||
)
|
)
|
||||||
Object.keys(notificationGroupsByDay).forEach((day) => {
|
Object.keys(notificationGroupsByDay).forEach((day) => {
|
||||||
// Group notifications by contract:
|
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
||||||
|
const bonusNotifications = notificationsGroupedByDay.filter(
|
||||||
|
(notification) => notification.sourceType === 'bonus'
|
||||||
|
)
|
||||||
|
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
||||||
|
(notification) => notification.sourceType !== 'bonus'
|
||||||
|
)
|
||||||
|
if (bonusNotifications.length > 0) {
|
||||||
|
notificationGroups = notificationGroups.concat({
|
||||||
|
notifications: bonusNotifications,
|
||||||
|
groupedById: 'income' + day,
|
||||||
|
isSeen: bonusNotifications[0].isSeen,
|
||||||
|
timePeriod: day,
|
||||||
|
type: 'income',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Group notifications by contract, filtering out bonuses:
|
||||||
const groupedNotificationsByContractId = groupBy(
|
const groupedNotificationsByContractId = groupBy(
|
||||||
notificationGroupsByDay[day],
|
normalNotificationsGroupedByDay,
|
||||||
(notification) => {
|
(notification) => {
|
||||||
return notification.sourceContractId
|
return notification.sourceContractId
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
notificationGroups = notificationGroups.concat(
|
notificationGroups = notificationGroups.concat(
|
||||||
map(groupedNotificationsByContractId, (notifications, contractId) => {
|
map(groupedNotificationsByContractId, (notifications, contractId) => {
|
||||||
|
const notificationsForContractId = groupedNotificationsByContractId[
|
||||||
|
contractId
|
||||||
|
].sort((a, b) => {
|
||||||
|
return b.createdTime - a.createdTime
|
||||||
|
})
|
||||||
// Create a notification group for each contract within each day
|
// Create a notification group for each contract within each day
|
||||||
const notificationGroup: NotificationGroup = {
|
const notificationGroup: NotificationGroup = {
|
||||||
notifications: groupedNotificationsByContractId[contractId].sort(
|
notifications: notificationsForContractId,
|
||||||
(a, b) => {
|
groupedById: contractId,
|
||||||
return b.createdTime - a.createdTime
|
isSeen: notificationsForContractId[0].isSeen,
|
||||||
}
|
|
||||||
),
|
|
||||||
sourceContractId: contractId,
|
|
||||||
isSeen: groupedNotificationsByContractId[contractId][0].isSeen,
|
|
||||||
timePeriod: day,
|
timePeriod: day,
|
||||||
|
type: 'normal',
|
||||||
}
|
}
|
||||||
return notificationGroup
|
return notificationGroup
|
||||||
})
|
})
|
||||||
|
@ -64,11 +83,11 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
return notificationGroups
|
return notificationGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePreferredNotifications(
|
export function usePreferredNotifications(
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
options: { unseenOnly: boolean }
|
options: { unseenOnly: boolean; customHref?: string }
|
||||||
) {
|
) {
|
||||||
const { unseenOnly } = options
|
const { unseenOnly, customHref } = options
|
||||||
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
|
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([])
|
const [notifications, setNotifications] = useState<Notification[]>([])
|
||||||
const [userAppropriateNotifications, setUserAppropriateNotifications] =
|
const [userAppropriateNotifications, setUserAppropriateNotifications] =
|
||||||
|
@ -93,9 +112,11 @@ function usePreferredNotifications(
|
||||||
const notificationsToShow = getAppropriateNotifications(
|
const notificationsToShow = getAppropriateNotifications(
|
||||||
notifications,
|
notifications,
|
||||||
privateUser.notificationPreferences
|
privateUser.notificationPreferences
|
||||||
|
).filter((n) =>
|
||||||
|
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
|
||||||
)
|
)
|
||||||
setUserAppropriateNotifications(notificationsToShow)
|
setUserAppropriateNotifications(notificationsToShow)
|
||||||
}, [privateUser, notifications])
|
}, [privateUser, notifications, customHref])
|
||||||
|
|
||||||
return userAppropriateNotifications
|
return userAppropriateNotifications
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,3 +73,7 @@ export function sellBet(params: any) {
|
||||||
export function createGroup(params: any) {
|
export function createGroup(params: any) {
|
||||||
return call(getFunctionUrl('creategroup'), 'POST', params)
|
return call(getFunctionUrl('creategroup'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestBonuses(params: any) {
|
||||||
|
return call(getFunctionUrl('getdailybonuses'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
|
@ -28,14 +28,32 @@ import { GroupSelector } from 'web/components/groups/group-selector'
|
||||||
import { CATEGORIES } from 'common/categories'
|
import { CATEGORIES } from 'common/categories'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
|
|
||||||
export default function Create() {
|
type NewQuestionParams = {
|
||||||
const [question, setQuestion] = useState('')
|
groupId?: string
|
||||||
// get query params:
|
q: string
|
||||||
const router = useRouter()
|
type: string
|
||||||
const { groupId } = router.query as { groupId: string }
|
description: string
|
||||||
useTracking('view create page')
|
closeTime: string
|
||||||
const creator = useUser()
|
outcomeType: string
|
||||||
|
// Params for PSEUDO_NUMERIC outcomeType
|
||||||
|
min?: string
|
||||||
|
max?: string
|
||||||
|
isLogScale?: string
|
||||||
|
initValue?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Create() {
|
||||||
|
useTracking('view create page')
|
||||||
|
const router = useRouter()
|
||||||
|
const params = router.query as NewQuestionParams
|
||||||
|
// TODO: Not sure why Question is pulled out as its own component;
|
||||||
|
// Maybe merge into newContract and then we don't need useEffect here.
|
||||||
|
const [question, setQuestion] = useState('')
|
||||||
|
useEffect(() => {
|
||||||
|
setQuestion(params.q ?? '')
|
||||||
|
}, [params.q])
|
||||||
|
|
||||||
|
const creator = useUser()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creator === null) router.push('/')
|
if (creator === null) router.push('/')
|
||||||
}, [creator, router])
|
}, [creator, router])
|
||||||
|
@ -65,11 +83,7 @@ export default function Create() {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
<NewContract
|
<NewContract question={question} params={params} creator={creator} />
|
||||||
question={question}
|
|
||||||
groupId={groupId}
|
|
||||||
creator={creator}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -80,20 +94,21 @@ export default function Create() {
|
||||||
export function NewContract(props: {
|
export function NewContract(props: {
|
||||||
creator: User
|
creator: User
|
||||||
question: string
|
question: string
|
||||||
groupId?: string
|
params?: NewQuestionParams
|
||||||
}) {
|
}) {
|
||||||
const { creator, question, groupId } = props
|
const { creator, question, params } = props
|
||||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
const { groupId, initValue } = params ?? {}
|
||||||
|
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
||||||
|
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
||||||
|
)
|
||||||
const [initialProb] = useState(50)
|
const [initialProb] = useState(50)
|
||||||
|
|
||||||
const [minString, setMinString] = useState('')
|
const [minString, setMinString] = useState(params?.min ?? '')
|
||||||
const [maxString, setMaxString] = useState('')
|
const [maxString, setMaxString] = useState(params?.max ?? '')
|
||||||
const [isLogScale, setIsLogScale] = useState(false)
|
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||||
const [initialValueString, setInitialValueString] = useState('')
|
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||||
|
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState(params?.description ?? '')
|
||||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
|
||||||
// const tags = parseWordsAsTags(tagText)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupId && creator)
|
if (groupId && creator)
|
||||||
getGroup(groupId).then((group) => {
|
getGroup(groupId).then((group) => {
|
||||||
|
@ -105,18 +120,17 @@ export function NewContract(props: {
|
||||||
}, [creator, groupId])
|
}, [creator, groupId])
|
||||||
const [ante, _setAnte] = useState(FIXED_ANTE)
|
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||||
|
|
||||||
// useEffect(() => {
|
// If params.closeTime is set, extract out the specified date and time
|
||||||
// if (ante === null && creator) {
|
|
||||||
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
|
||||||
// setAnte(initialAnte)
|
|
||||||
// }
|
|
||||||
// }, [ante, creator])
|
|
||||||
|
|
||||||
// const [anteError, setAnteError] = useState<string | undefined>()
|
|
||||||
// By default, close the market a week from today
|
// By default, close the market a week from today
|
||||||
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
|
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
|
||||||
const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday)
|
const timeInMs = Number(params?.closeTime ?? 0)
|
||||||
const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>('23:59')
|
const initDate = timeInMs
|
||||||
|
? dayjs(timeInMs).format('YYYY-MM-DD')
|
||||||
|
: weekFromToday
|
||||||
|
const initTime = timeInMs ? dayjs(timeInMs).format('HH:mm') : '23:59'
|
||||||
|
const [closeDate, setCloseDate] = useState<undefined | string>(initDate)
|
||||||
|
const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>(initTime)
|
||||||
|
|
||||||
const [marketInfoText, setMarketInfoText] = useState('')
|
const [marketInfoText, setMarketInfoText] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(
|
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import {
|
import { Notification } from 'common/notification'
|
||||||
Notification,
|
|
||||||
notification_reason_types,
|
|
||||||
notification_source_types,
|
|
||||||
notification_source_update_types,
|
|
||||||
} from 'common/notification'
|
|
||||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
|
@ -31,47 +26,40 @@ import {
|
||||||
ProbPercentLabel,
|
ProbPercentLabel,
|
||||||
} from 'web/components/outcome-label'
|
} from 'web/components/outcome-label'
|
||||||
import {
|
import {
|
||||||
groupNotifications,
|
|
||||||
NotificationGroup,
|
NotificationGroup,
|
||||||
usePreferredGroupedNotifications,
|
usePreferredGroupedNotifications,
|
||||||
} from 'web/hooks/use-notifications'
|
} from 'web/hooks/use-notifications'
|
||||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
import { CheckIcon, TrendingUpIcon, XIcon } from '@heroicons/react/outline'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
||||||
|
import { groupBy } from 'lodash'
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_PER_PAGE = 30
|
||||||
|
export const HIGHLIGHT_DURATION = 30 * 1000
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [unseenNotificationGroups, setUnseenNotificationGroups] = useState<
|
const [page, setPage] = useState(1)
|
||||||
NotificationGroup[] | undefined
|
|
||||||
>(undefined)
|
const groupedNotifications = usePreferredGroupedNotifications(user?.id, {
|
||||||
const allNotificationGroups = usePreferredGroupedNotifications(user?.id, {
|
|
||||||
unseenOnly: false,
|
unseenOnly: false,
|
||||||
})
|
})
|
||||||
|
const [paginatedNotificationGroups, setPaginatedNotificationGroups] =
|
||||||
|
useState<NotificationGroup[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!allNotificationGroups) return
|
if (!groupedNotifications) return
|
||||||
// Don't re-add notifications that are visible right now or have been seen already.
|
const start = (page - 1) * NOTIFICATIONS_PER_PAGE
|
||||||
const currentlyVisibleUnseenNotificationIds = Object.values(
|
const end = start + NOTIFICATIONS_PER_PAGE
|
||||||
unseenNotificationGroups ?? []
|
const maxNotificationsToShow = groupedNotifications.slice(start, end)
|
||||||
)
|
const remainingNotification = groupedNotifications.slice(end)
|
||||||
.map((n) => n.notifications.map((n) => n.id))
|
for (const notification of remainingNotification) {
|
||||||
.flat()
|
if (notification.isSeen) break
|
||||||
const unseenGroupedNotifications = groupNotifications(
|
else setNotificationsAsSeen(notification.notifications)
|
||||||
allNotificationGroups
|
}
|
||||||
.map((notification: NotificationGroup) => notification.notifications)
|
setPaginatedNotificationGroups(maxNotificationsToShow)
|
||||||
.flat()
|
}, [groupedNotifications, page])
|
||||||
.filter(
|
|
||||||
(notification: Notification) =>
|
|
||||||
!notification.isSeen ||
|
|
||||||
currentlyVisibleUnseenNotificationIds.includes(notification.id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setUnseenNotificationGroups(unseenGroupedNotifications)
|
|
||||||
|
|
||||||
// We don't want unseenNotificationsGroup to be in the dependencies as we update it here.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [allNotificationGroups])
|
|
||||||
|
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
|
@ -80,7 +68,6 @@ export default function Notifications() {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use infinite scroll
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className={'p-2 sm:p-4'}>
|
<div className={'p-2 sm:p-4'}>
|
||||||
|
@ -90,53 +77,74 @@ export default function Notifications() {
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
title: 'New Notifications',
|
title: 'Notifications',
|
||||||
content: unseenNotificationGroups ? (
|
content: groupedNotifications ? (
|
||||||
<div className={''}>
|
<div className={''}>
|
||||||
{unseenNotificationGroups.length === 0 &&
|
{paginatedNotificationGroups.length === 0 &&
|
||||||
"You don't have any new notifications."}
|
"You don't have any notifications. Try changing your settings to see more."}
|
||||||
{unseenNotificationGroups.map((notification) =>
|
{paginatedNotificationGroups.map((notification) =>
|
||||||
notification.notifications.length === 1 ? (
|
notification.notifications.length === 1 ? (
|
||||||
<NotificationItem
|
<NotificationItem
|
||||||
notification={notification.notifications[0]}
|
notification={notification.notifications[0]}
|
||||||
key={notification.notifications[0].id}
|
key={notification.notifications[0].id}
|
||||||
/>
|
/>
|
||||||
|
) : notification.type === 'income' ? (
|
||||||
|
<IncomeNotificationGroupItem
|
||||||
|
notificationGroup={notification}
|
||||||
|
key={notification.groupedById + notification.timePeriod}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<NotificationGroupItem
|
<NotificationGroupItem
|
||||||
notificationGroup={notification}
|
notificationGroup={notification}
|
||||||
key={
|
key={notification.groupedById + notification.timePeriod}
|
||||||
notification.sourceContractId +
|
|
||||||
notification.timePeriod
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
{groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
|
||||||
) : (
|
<nav
|
||||||
<LoadingIndicator />
|
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
|
||||||
),
|
aria-label="Pagination"
|
||||||
},
|
>
|
||||||
{
|
<div className="hidden sm:block">
|
||||||
title: 'All Notifications',
|
<p className="text-sm text-gray-700">
|
||||||
content: allNotificationGroups ? (
|
Showing{' '}
|
||||||
<div className={''}>
|
<span className="font-medium">
|
||||||
{allNotificationGroups.length === 0 &&
|
{page === 1
|
||||||
"You don't have any notifications. Try changing your settings to see more."}
|
? page
|
||||||
{allNotificationGroups.map((notification) =>
|
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
||||||
notification.notifications.length === 1 ? (
|
</span>{' '}
|
||||||
<NotificationItem
|
to{' '}
|
||||||
notification={notification.notifications[0]}
|
<span className="font-medium">
|
||||||
key={notification.notifications[0].id}
|
{page * NOTIFICATIONS_PER_PAGE}
|
||||||
/>
|
</span>{' '}
|
||||||
) : (
|
of{' '}
|
||||||
<NotificationGroupItem
|
<span className="font-medium">
|
||||||
notificationGroup={notification}
|
{groupedNotifications.length}
|
||||||
key={
|
</span>{' '}
|
||||||
notification.sourceContractId +
|
results
|
||||||
notification.timePeriod
|
</p>
|
||||||
}
|
</div>
|
||||||
/>
|
<div className="flex flex-1 justify-between sm:justify-end">
|
||||||
)
|
<a
|
||||||
|
href="#"
|
||||||
|
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
onClick={() => page > 1 && setPage(page - 1)}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
|
onClick={() =>
|
||||||
|
page <
|
||||||
|
groupedNotifications?.length /
|
||||||
|
NOTIFICATIONS_PER_PAGE && setPage(page + 1)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -158,13 +166,12 @@ export default function Notifications() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setNotificationsAsSeen = (notifications: Notification[]) => {
|
export const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||||
notifications.forEach((notification) => {
|
notifications.forEach((notification) => {
|
||||||
if (!notification.isSeen)
|
if (!notification.isSeen)
|
||||||
updateDoc(
|
updateDoc(
|
||||||
doc(db, `users/${notification.userId}/notifications/`, notification.id),
|
doc(db, `users/${notification.userId}/notifications/`, notification.id),
|
||||||
{
|
{
|
||||||
...notification,
|
|
||||||
isSeen: true,
|
isSeen: true,
|
||||||
viewTime: new Date(),
|
viewTime: new Date(),
|
||||||
}
|
}
|
||||||
|
@ -173,6 +180,152 @@ const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||||
return notifications
|
return notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IncomeNotificationGroupItem(props: {
|
||||||
|
notificationGroup: NotificationGroup
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { notificationGroup, className } = props
|
||||||
|
const { notifications } = notificationGroup
|
||||||
|
const numSummaryLines = 3
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (notifications.some((n) => !n.isSeen)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
|
setNotificationsAsSeen(notifications)
|
||||||
|
}, [notifications])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded) setHighlighted(false)
|
||||||
|
}, [expanded])
|
||||||
|
|
||||||
|
const totalIncome = notifications.reduce(
|
||||||
|
(acc, notification) =>
|
||||||
|
acc +
|
||||||
|
(notification.sourceType &&
|
||||||
|
notification.sourceText &&
|
||||||
|
notification.sourceType === 'bonus'
|
||||||
|
? parseInt(notification.sourceText)
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
// loop through the contracts and combine the notification items into one
|
||||||
|
function combineNotificationsByAddingSourceTextsAndReturningTheRest(
|
||||||
|
notifications: Notification[]
|
||||||
|
) {
|
||||||
|
const newNotifications = []
|
||||||
|
const groupedNotificationsByContractId = groupBy(
|
||||||
|
notifications,
|
||||||
|
(notification) => {
|
||||||
|
return notification.sourceContractId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for (const contractId in groupedNotificationsByContractId) {
|
||||||
|
const notificationsForContractId =
|
||||||
|
groupedNotificationsByContractId[contractId]
|
||||||
|
let sum = 0
|
||||||
|
notificationsForContractId.forEach(
|
||||||
|
(notification) =>
|
||||||
|
notification.sourceText &&
|
||||||
|
(sum = parseInt(notification.sourceText) + sum)
|
||||||
|
)
|
||||||
|
|
||||||
|
const newNotification =
|
||||||
|
notificationsForContractId.length === 1
|
||||||
|
? notificationsForContractId[0]
|
||||||
|
: {
|
||||||
|
...notificationsForContractId[0],
|
||||||
|
sourceText: sum.toString(),
|
||||||
|
}
|
||||||
|
newNotifications.push(newNotification)
|
||||||
|
}
|
||||||
|
return newNotifications
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedNotifs =
|
||||||
|
combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
||||||
|
className,
|
||||||
|
!expanded ? 'hover:bg-gray-100' : '',
|
||||||
|
highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : ''
|
||||||
|
)}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded && (
|
||||||
|
<span
|
||||||
|
className="absolute top-14 left-6 -ml-px h-[calc(100%-5rem)] w-0.5 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
|
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||||
|
<div className={'flex-1 overflow-hidden pl-2 sm:flex'}>
|
||||||
|
<div
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{'Daily Income Summary: '}
|
||||||
|
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<RelativeTimestamp time={notifications[0].createdTime} />
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}>
|
||||||
|
{' '}
|
||||||
|
<div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}>
|
||||||
|
{!expanded ? (
|
||||||
|
<>
|
||||||
|
{combinedNotifs
|
||||||
|
.slice(0, numSummaryLines)
|
||||||
|
.map((notification) => {
|
||||||
|
return (
|
||||||
|
<NotificationItem
|
||||||
|
notification={notification}
|
||||||
|
justSummary={true}
|
||||||
|
key={notification.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div className={'text-sm text-gray-500 hover:underline '}>
|
||||||
|
{combinedNotifs.length - numSummaryLines > 0
|
||||||
|
? 'And ' +
|
||||||
|
(combinedNotifs.length - numSummaryLines) +
|
||||||
|
' more...'
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{combinedNotifs.map((notification) => (
|
||||||
|
<NotificationItem
|
||||||
|
notification={notification}
|
||||||
|
key={notification.id}
|
||||||
|
justSummary={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mt-6 border-b border-gray-300'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function NotificationGroupItem(props: {
|
function NotificationGroupItem(props: {
|
||||||
notificationGroup: NotificationGroup
|
notificationGroup: NotificationGroup
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -187,17 +340,28 @@ function NotificationGroupItem(props: {
|
||||||
const numSummaryLines = 3
|
const numSummaryLines = 3
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (notifications.some((n) => !n.isSeen)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
setNotificationsAsSeen(notifications)
|
setNotificationsAsSeen(notifications)
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded) setHighlighted(false)
|
||||||
|
}, [expanded])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
||||||
className,
|
className,
|
||||||
!expanded ? 'hover:bg-gray-100' : ''
|
!expanded ? 'hover:bg-gray-100' : '',
|
||||||
|
highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : ''
|
||||||
)}
|
)}
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
>
|
>
|
||||||
|
@ -432,7 +596,7 @@ function NotificationSettings() {
|
||||||
/>
|
/>
|
||||||
<NotificationSettingLine
|
<NotificationSettingLine
|
||||||
highlight={notificationSettings !== 'none'}
|
highlight={notificationSettings !== 'none'}
|
||||||
label={"Referral bonuses you've received"}
|
label={"Income & referral bonuses you've received"}
|
||||||
/>
|
/>
|
||||||
<NotificationSettingLine
|
<NotificationSettingLine
|
||||||
label={"Activity on questions you've ever bet or commented on"}
|
label={"Activity on questions you've ever bet or commented on"}
|
||||||
|
@ -476,17 +640,6 @@ function NotificationSettings() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotificationAboutContractResolution(
|
|
||||||
sourceType: notification_source_types | undefined,
|
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
|
||||||
contract: Contract | null | undefined
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
(sourceType === 'contract' && sourceUpdateType === 'resolved') ||
|
|
||||||
(sourceType === 'contract' && !sourceUpdateType && contract?.resolution)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationItem(props: {
|
function NotificationItem(props: {
|
||||||
notification: Notification
|
notification: Notification
|
||||||
justSummary?: boolean
|
justSummary?: boolean
|
||||||
|
@ -522,6 +675,16 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
}, [reasonText, sourceText])
|
}, [reasonText, sourceText])
|
||||||
|
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!notification.isSeen) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
|
}, [notification.isSeen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotificationsAsSeen([notification])
|
setNotificationsAsSeen([notification])
|
||||||
}, [notification])
|
}, [notification])
|
||||||
|
@ -559,22 +722,21 @@ function NotificationItem(props: {
|
||||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||||
<div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}>
|
<div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}>
|
||||||
<div className={'flex pl-1 sm:pl-0'}>
|
<div className={'flex pl-1 sm:pl-0'}>
|
||||||
<UserLink
|
{sourceType != 'bonus' && (
|
||||||
name={sourceUserName || ''}
|
<UserLink
|
||||||
username={sourceUserUsername || ''}
|
name={sourceUserName || ''}
|
||||||
className={'mr-0 flex-shrink-0'}
|
username={sourceUserUsername || ''}
|
||||||
/>
|
className={'mr-0 flex-shrink-0'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||||
<span className={'flex-shrink-0'}>
|
<span className={'flex-shrink-0'}>
|
||||||
{sourceType &&
|
{sourceType &&
|
||||||
reason &&
|
reason &&
|
||||||
getReasonForShowingNotification(
|
getReasonForShowingNotification(notification, true).replace(
|
||||||
sourceType,
|
' on',
|
||||||
reason,
|
''
|
||||||
sourceUpdateType,
|
)}
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
).replace(' on', '')}
|
|
||||||
</span>
|
</span>
|
||||||
<div className={'ml-1 text-black'}>
|
<div className={'ml-1 text-black'}>
|
||||||
<NotificationTextLabel
|
<NotificationTextLabel
|
||||||
|
@ -593,37 +755,41 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-white px-2 pt-6 text-sm sm:px-4'}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-white px-2 pt-6 text-sm sm:px-4',
|
||||||
|
highlighted && 'bg-indigo-200 hover:bg-indigo-100'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<a href={getSourceUrl()}>
|
<a href={getSourceUrl()}>
|
||||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
<Avatar
|
{sourceType != 'bonus' ? (
|
||||||
avatarUrl={sourceUserAvatarUrl}
|
<Avatar
|
||||||
size={'sm'}
|
avatarUrl={sourceUserAvatarUrl}
|
||||||
className={'mr-2'}
|
size={'sm'}
|
||||||
username={sourceUserName}
|
className={'mr-2'}
|
||||||
/>
|
username={sourceUserName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||||
|
)}
|
||||||
<div className={'flex-1 overflow-hidden sm:flex'}>
|
<div className={'flex-1 overflow-hidden sm:flex'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UserLink
|
{sourceType != 'bonus' && sourceUpdateType != 'closed' && (
|
||||||
name={sourceUserName || ''}
|
<UserLink
|
||||||
username={sourceUserUsername || ''}
|
name={sourceUserName || ''}
|
||||||
className={'mr-0 flex-shrink-0'}
|
username={sourceUserUsername || ''}
|
||||||
/>
|
className={'mr-0 flex-shrink-0'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||||
{sourceType && reason && (
|
{sourceType && reason && (
|
||||||
<div className={'inline truncate'}>
|
<div className={'inline truncate'}>
|
||||||
{getReasonForShowingNotification(
|
{getReasonForShowingNotification(notification, false)}
|
||||||
sourceType,
|
|
||||||
reason,
|
|
||||||
sourceUpdateType,
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
sourceSlug
|
|
||||||
)}
|
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
sourceContractCreatorUsername
|
sourceContractCreatorUsername
|
||||||
|
@ -684,13 +850,7 @@ function NotificationTextLabel(props: {
|
||||||
return <span>{contract?.question || sourceContractTitle}</span>
|
return <span>{contract?.question || sourceContractTitle}</span>
|
||||||
if (!sourceText) return <div />
|
if (!sourceText) return <div />
|
||||||
// Resolved contracts
|
// Resolved contracts
|
||||||
if (
|
if (sourceType === 'contract' && sourceUpdateType === 'resolved') {
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
{
|
{
|
||||||
if (sourceText === 'YES' || sourceText == 'NO') {
|
if (sourceText === 'YES' || sourceText == 'NO') {
|
||||||
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
||||||
|
@ -730,6 +890,12 @@ function NotificationTextLabel(props: {
|
||||||
return (
|
return (
|
||||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||||
)
|
)
|
||||||
|
} else if (sourceType === 'bonus' && sourceText) {
|
||||||
|
return (
|
||||||
|
<span className="text-primary">
|
||||||
|
{'+' + formatMoney(parseInt(sourceText))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// return default text
|
// return default text
|
||||||
return (
|
return (
|
||||||
|
@ -740,15 +906,13 @@ function NotificationTextLabel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReasonForShowingNotification(
|
function getReasonForShowingNotification(
|
||||||
source: notification_source_types,
|
notification: Notification,
|
||||||
reason: notification_reason_types,
|
simple?: boolean
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
|
||||||
contract: Contract | undefined | null,
|
|
||||||
simple?: boolean,
|
|
||||||
sourceSlug?: string
|
|
||||||
) {
|
) {
|
||||||
|
const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } =
|
||||||
|
notification
|
||||||
let reasonText: string
|
let reasonText: string
|
||||||
switch (source) {
|
switch (sourceType) {
|
||||||
case 'comment':
|
case 'comment':
|
||||||
if (reason === 'reply_to_users_answer')
|
if (reason === 'reply_to_users_answer')
|
||||||
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
||||||
|
@ -768,16 +932,9 @@ function getReasonForShowingNotification(
|
||||||
break
|
break
|
||||||
case 'contract':
|
case 'contract':
|
||||||
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
||||||
else if (
|
else if (sourceUpdateType === 'resolved') reasonText = `resolved`
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
source,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
)
|
|
||||||
reasonText = `resolved`
|
|
||||||
else if (sourceUpdateType === 'closed')
|
else if (sourceUpdateType === 'closed')
|
||||||
reasonText = `please resolve your question`
|
reasonText = `Please resolve your question`
|
||||||
else reasonText = `updated`
|
else reasonText = `updated`
|
||||||
break
|
break
|
||||||
case 'answer':
|
case 'answer':
|
||||||
|
@ -805,6 +962,15 @@ function getReasonForShowingNotification(
|
||||||
else if (sourceSlug) reasonText = 'joined because you shared'
|
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||||
else reasonText = 'joined because of you'
|
else reasonText = 'joined because of you'
|
||||||
break
|
break
|
||||||
|
case 'bonus':
|
||||||
|
if (reason === 'unique_bettors_on_your_contract' && sourceText)
|
||||||
|
reasonText = !simple
|
||||||
|
? `You had ${
|
||||||
|
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||||
|
} unique bettors on`
|
||||||
|
: 'You earned Mana for unique bettors:'
|
||||||
|
else reasonText = 'You earned your daily manna'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
reasonText = ''
|
reasonText = ''
|
||||||
}
|
}
|
||||||
|
|
208
yarn.lock
208
yarn.lock
|
@ -2181,6 +2181,20 @@
|
||||||
google-gax "^2.24.1"
|
google-gax "^2.24.1"
|
||||||
protobufjs "^6.8.6"
|
protobufjs "^6.8.6"
|
||||||
|
|
||||||
|
"@google-cloud/functions-framework@3.1.2":
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@google-cloud/functions-framework/-/functions-framework-3.1.2.tgz#2cd92ce4307bf7f32555d028dca22e398473b410"
|
||||||
|
integrity sha512-pYvEH65/Rqh1JNPdcBmorcV7Xoom2/iOSmbtYza8msro7Inl+qOYxbyMiQfySD2gwAyn38WyWPRqsDRcf/BFLg==
|
||||||
|
dependencies:
|
||||||
|
"@types/express" "4.17.13"
|
||||||
|
body-parser "^1.18.3"
|
||||||
|
cloudevents "^6.0.0"
|
||||||
|
express "^4.16.4"
|
||||||
|
minimist "^1.2.5"
|
||||||
|
on-finished "^2.3.0"
|
||||||
|
read-pkg-up "^7.0.1"
|
||||||
|
semver "^7.3.5"
|
||||||
|
|
||||||
"@google-cloud/paginator@^3.0.7":
|
"@google-cloud/paginator@^3.0.7":
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b"
|
resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b"
|
||||||
|
@ -2926,7 +2940,7 @@
|
||||||
"@types/qs" "*"
|
"@types/qs" "*"
|
||||||
"@types/range-parser" "*"
|
"@types/range-parser" "*"
|
||||||
|
|
||||||
"@types/express@*", "@types/express@^4.17.13":
|
"@types/express@*", "@types/express@4.17.13", "@types/express@^4.17.13":
|
||||||
version "4.17.13"
|
version "4.17.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
||||||
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||||
|
@ -3049,6 +3063,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
|
||||||
integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==
|
integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==
|
||||||
|
|
||||||
|
"@types/normalize-package-data@^2.4.0":
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
|
||||||
|
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
|
@ -3498,7 +3517,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
|
||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ajv@^8.0.0, ajv@^8.8.0:
|
ajv@^8.0.0, ajv@^8.11.0, ajv@^8.8.0:
|
||||||
version "8.11.0"
|
version "8.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
|
||||||
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
|
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
|
||||||
|
@ -3750,6 +3769,11 @@ autoprefixer@^10.3.7, autoprefixer@^10.4.2:
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
postcss-value-parser "^4.2.0"
|
postcss-value-parser "^4.2.0"
|
||||||
|
|
||||||
|
available-typed-arrays@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
||||||
|
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
|
||||||
|
|
||||||
axe-core@^4.3.5:
|
axe-core@^4.3.5:
|
||||||
version "4.4.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.2.tgz#dcf7fb6dea866166c3eab33d68208afe4d5f670c"
|
||||||
|
@ -3880,7 +3904,7 @@ bluebird@^3.7.1:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
body-parser@1.20.0:
|
body-parser@1.20.0, body-parser@^1.18.3:
|
||||||
version "1.20.0"
|
version "1.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5"
|
||||||
integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==
|
integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==
|
||||||
|
@ -4236,6 +4260,16 @@ clone-response@^1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response "^1.0.0"
|
mimic-response "^1.0.0"
|
||||||
|
|
||||||
|
cloudevents@^6.0.0:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cloudevents/-/cloudevents-6.0.2.tgz#7b4990a92c6c30f6790eb4b59207b4d8949fca12"
|
||||||
|
integrity sha512-mn/4EZnAbhfb/TghubK2jPnxYM15JRjf8LnWJtXidiVKi5ZCkd+p9jyBZbL57w7nRm6oFAzJhjxRLsXd/DNaBQ==
|
||||||
|
dependencies:
|
||||||
|
ajv "^8.11.0"
|
||||||
|
ajv-formats "^2.1.1"
|
||||||
|
util "^0.12.4"
|
||||||
|
uuid "^8.3.2"
|
||||||
|
|
||||||
clsx@1.1.1, clsx@^1.1.1:
|
clsx@1.1.1, clsx@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||||
|
@ -5277,7 +5311,7 @@ error-ex@^1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-arrayish "^0.2.1"
|
is-arrayish "^0.2.1"
|
||||||
|
|
||||||
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
|
es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0:
|
||||||
version "1.20.1"
|
version "1.20.1"
|
||||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
|
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
|
||||||
integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
|
integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
|
||||||
|
@ -5657,7 +5691,7 @@ execa@^5.0.0:
|
||||||
signal-exit "^3.0.3"
|
signal-exit "^3.0.3"
|
||||||
strip-final-newline "^2.0.0"
|
strip-final-newline "^2.0.0"
|
||||||
|
|
||||||
express@^4.17.1, express@^4.17.3:
|
express@^4.16.4, express@^4.17.1, express@^4.17.3:
|
||||||
version "4.18.1"
|
version "4.18.1"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
||||||
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
|
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
|
||||||
|
@ -5871,7 +5905,7 @@ find-up@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path "^3.0.0"
|
locate-path "^3.0.0"
|
||||||
|
|
||||||
find-up@^4.0.0:
|
find-up@^4.0.0, find-up@^4.1.0:
|
||||||
version "4.1.0"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||||
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
|
||||||
|
@ -5981,6 +6015,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.7:
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
|
||||||
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
|
integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==
|
||||||
|
|
||||||
|
for-each@^0.3.3:
|
||||||
|
version "0.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
|
||||||
|
integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
|
||||||
|
dependencies:
|
||||||
|
is-callable "^1.1.3"
|
||||||
|
|
||||||
fork-ts-checker-webpack-plugin@^6.5.0:
|
fork-ts-checker-webpack-plugin@^6.5.0:
|
||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340"
|
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.2.tgz#4f67183f2f9eb8ba7df7177ce3cf3e75cdafb340"
|
||||||
|
@ -6585,6 +6626,11 @@ hoist-non-react-statics@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is "^16.7.0"
|
react-is "^16.7.0"
|
||||||
|
|
||||||
|
hosted-git-info@^2.1.4:
|
||||||
|
version "2.8.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||||
|
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||||
|
|
||||||
hpack.js@^2.1.6:
|
hpack.js@^2.1.6:
|
||||||
version "2.1.6"
|
version "2.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
|
resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
|
||||||
|
@ -6945,6 +6991,14 @@ is-alphanumerical@^1.0.0:
|
||||||
is-alphabetical "^1.0.0"
|
is-alphabetical "^1.0.0"
|
||||||
is-decimal "^1.0.0"
|
is-decimal "^1.0.0"
|
||||||
|
|
||||||
|
is-arguments@^1.0.4:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
||||||
|
integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
is-arrayish@^0.2.1:
|
is-arrayish@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||||
|
@ -6977,7 +7031,7 @@ is-buffer@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
|
||||||
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
|
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
|
||||||
|
|
||||||
is-callable@^1.1.4, is-callable@^1.2.4:
|
is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
|
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
|
||||||
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
|
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
|
||||||
|
@ -6989,7 +7043,7 @@ is-ci@^2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ci-info "^2.0.0"
|
ci-info "^2.0.0"
|
||||||
|
|
||||||
is-core-module@^2.2.0, is-core-module@^2.8.1:
|
is-core-module@^2.2.0, is-core-module@^2.8.1, is-core-module@^2.9.0:
|
||||||
version "2.9.0"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
|
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
|
||||||
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
|
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==
|
||||||
|
@ -7028,6 +7082,13 @@ is-fullwidth-code-point@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
|
||||||
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
|
||||||
|
|
||||||
|
is-generator-function@^1.0.7:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
|
||||||
|
integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
|
||||||
|
dependencies:
|
||||||
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
|
||||||
|
@ -7161,6 +7222,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols "^1.0.2"
|
has-symbols "^1.0.2"
|
||||||
|
|
||||||
|
is-typed-array@^1.1.3, is-typed-array@^1.1.9:
|
||||||
|
version "1.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67"
|
||||||
|
integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==
|
||||||
|
dependencies:
|
||||||
|
available-typed-arrays "^1.0.5"
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
es-abstract "^1.20.0"
|
||||||
|
for-each "^0.3.3"
|
||||||
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
is-typedarray@^1.0.0:
|
is-typedarray@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||||
|
@ -8126,6 +8198,16 @@ nopt@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
abbrev "1"
|
abbrev "1"
|
||||||
|
|
||||||
|
normalize-package-data@^2.5.0:
|
||||||
|
version "2.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
|
||||||
|
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
|
||||||
|
dependencies:
|
||||||
|
hosted-git-info "^2.1.4"
|
||||||
|
resolve "^1.10.0"
|
||||||
|
semver "2 || 3 || 4 || 5"
|
||||||
|
validate-npm-package-license "^3.0.1"
|
||||||
|
|
||||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
|
@ -8252,7 +8334,7 @@ obuf@^1.0.0, obuf@^1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
||||||
|
|
||||||
on-finished@2.4.1:
|
on-finished@2.4.1, on-finished@^2.3.0:
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||||
|
@ -9463,6 +9545,25 @@ react@17.0.2, react@^17.0.1:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
|
|
||||||
|
read-pkg-up@^7.0.1:
|
||||||
|
version "7.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
|
||||||
|
integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
|
||||||
|
dependencies:
|
||||||
|
find-up "^4.1.0"
|
||||||
|
read-pkg "^5.2.0"
|
||||||
|
type-fest "^0.8.1"
|
||||||
|
|
||||||
|
read-pkg@^5.2.0:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
|
||||||
|
integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
|
||||||
|
dependencies:
|
||||||
|
"@types/normalize-package-data" "^2.4.0"
|
||||||
|
normalize-package-data "^2.5.0"
|
||||||
|
parse-json "^5.0.0"
|
||||||
|
type-fest "^0.6.0"
|
||||||
|
|
||||||
readable-stream@1.1.x:
|
readable-stream@1.1.x:
|
||||||
version "1.1.14"
|
version "1.1.14"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
|
||||||
|
@ -9767,6 +9868,15 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.
|
||||||
path-parse "^1.0.7"
|
path-parse "^1.0.7"
|
||||||
supports-preserve-symlinks-flag "^1.0.0"
|
supports-preserve-symlinks-flag "^1.0.0"
|
||||||
|
|
||||||
|
resolve@^1.10.0:
|
||||||
|
version "1.22.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||||
|
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
|
||||||
|
dependencies:
|
||||||
|
is-core-module "^2.9.0"
|
||||||
|
path-parse "^1.0.7"
|
||||||
|
supports-preserve-symlinks-flag "^1.0.0"
|
||||||
|
|
||||||
resolve@^2.0.0-next.3:
|
resolve@^2.0.0-next.3:
|
||||||
version "2.0.0-next.3"
|
version "2.0.0-next.3"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
|
||||||
|
@ -9848,7 +9958,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
|
||||||
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
|
||||||
version "5.2.1"
|
version "5.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||||
|
@ -9959,16 +10069,16 @@ semver-diff@^3.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
|
"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.6.0:
|
||||||
|
version "5.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
semver@7.0.0:
|
semver@7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||||
|
|
||||||
semver@^5.4.1, semver@^5.6.0:
|
|
||||||
version "5.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
|
||||||
version "6.3.0"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
|
@ -10223,6 +10333,32 @@ spawn-command@^0.0.2-1:
|
||||||
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
|
resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0"
|
||||||
integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
|
integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=
|
||||||
|
|
||||||
|
spdx-correct@^3.0.0:
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
|
||||||
|
integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
|
||||||
|
dependencies:
|
||||||
|
spdx-expression-parse "^3.0.0"
|
||||||
|
spdx-license-ids "^3.0.0"
|
||||||
|
|
||||||
|
spdx-exceptions@^2.1.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
|
||||||
|
integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
|
||||||
|
|
||||||
|
spdx-expression-parse@^3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
|
||||||
|
integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
|
||||||
|
dependencies:
|
||||||
|
spdx-exceptions "^2.1.0"
|
||||||
|
spdx-license-ids "^3.0.0"
|
||||||
|
|
||||||
|
spdx-license-ids@^3.0.0:
|
||||||
|
version "3.0.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95"
|
||||||
|
integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==
|
||||||
|
|
||||||
spdy-transport@^3.0.0:
|
spdy-transport@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
|
||||||
|
@ -10706,6 +10842,16 @@ type-fest@^0.20.2:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
|
type-fest@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
|
||||||
|
integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
|
||||||
|
|
||||||
|
type-fest@^0.8.1:
|
||||||
|
version "0.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||||
|
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||||
|
|
||||||
type-fest@^2.5.0:
|
type-fest@^2.5.0:
|
||||||
version "2.13.0"
|
version "2.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.13.0.tgz#d1ecee38af29eb2e863b22299a3d68ef30d2abfb"
|
||||||
|
@ -10974,6 +11120,18 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||||
|
|
||||||
|
util@^0.12.4:
|
||||||
|
version "0.12.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
|
||||||
|
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
|
||||||
|
dependencies:
|
||||||
|
inherits "^2.0.3"
|
||||||
|
is-arguments "^1.0.4"
|
||||||
|
is-generator-function "^1.0.7"
|
||||||
|
is-typed-array "^1.1.3"
|
||||||
|
safe-buffer "^5.1.2"
|
||||||
|
which-typed-array "^1.1.2"
|
||||||
|
|
||||||
utila@~0.4:
|
utila@~0.4:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
|
||||||
|
@ -10999,6 +11157,14 @@ v8-compile-cache@^2.0.3:
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
|
||||||
|
|
||||||
|
validate-npm-package-license@^3.0.1:
|
||||||
|
version "3.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||||
|
integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
|
||||||
|
dependencies:
|
||||||
|
spdx-correct "^3.0.0"
|
||||||
|
spdx-expression-parse "^3.0.0"
|
||||||
|
|
||||||
value-equal@^1.0.1:
|
value-equal@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||||
|
@ -11232,6 +11398,18 @@ which-boxed-primitive@^1.0.2:
|
||||||
is-string "^1.0.5"
|
is-string "^1.0.5"
|
||||||
is-symbol "^1.0.3"
|
is-symbol "^1.0.3"
|
||||||
|
|
||||||
|
which-typed-array@^1.1.2:
|
||||||
|
version "1.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
|
||||||
|
integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==
|
||||||
|
dependencies:
|
||||||
|
available-typed-arrays "^1.0.5"
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
es-abstract "^1.20.0"
|
||||||
|
for-each "^0.3.3"
|
||||||
|
has-tostringtag "^1.0.0"
|
||||||
|
is-typed-array "^1.1.9"
|
||||||
|
|
||||||
which@^1.3.1:
|
which@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user