Merge branch 'main' into atlas2
This commit is contained in:
commit
f622eaca88
|
@ -1,6 +1,7 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['lib'],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
@ -31,6 +32,7 @@ module.exports = {
|
|||
rules: {
|
||||
'no-extra-semi': 'off',
|
||||
'no-constant-condition': ['error', { checkLoops: false }],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ import { noFees } from './fees'
|
|||
import { addObjects } from './util/object'
|
||||
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 = {
|
||||
newBet: CandidateBet<Bet>
|
||||
newBet: CandidateBet
|
||||
newPool?: { [outcome: string]: number }
|
||||
newTotalShares?: { [outcome: string]: number }
|
||||
newTotalBets?: { [outcome: string]: number }
|
||||
|
@ -46,7 +46,7 @@ export const getNewBinaryCpmmBetInfo = (
|
|||
const probBefore = getCpmmProbability(pool, p)
|
||||
const probAfter = getCpmmProbability(newPool, newP)
|
||||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
shares,
|
||||
|
@ -96,7 +96,7 @@ export const getNewBinaryDpmBetInfo = (
|
|||
const probBefore = getDpmProbability(contract.totalShares)
|
||||
const probAfter = getDpmProbability(newTotalShares)
|
||||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
|
@ -133,7 +133,7 @@ export const getNewMultiBetInfo = (
|
|||
const probBefore = getDpmOutcomeProbability(totalShares, outcome)
|
||||
const probAfter = getDpmOutcomeProbability(newTotalShares, outcome)
|
||||
|
||||
const newBet: CandidateBet<Bet> = {
|
||||
const newBet: CandidateBet = {
|
||||
contractId: contract.id,
|
||||
amount,
|
||||
loanAmount,
|
||||
|
|
|
@ -22,6 +22,8 @@ export type Notification = {
|
|||
|
||||
sourceSlug?: string
|
||||
sourceTitle?: string
|
||||
|
||||
isSeenOnHref?: string
|
||||
}
|
||||
export type notification_source_types =
|
||||
| 'contract'
|
||||
|
@ -34,6 +36,7 @@ export type notification_source_types =
|
|||
| 'admin_message'
|
||||
| 'group'
|
||||
| 'user'
|
||||
| 'bonus'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -56,3 +59,5 @@ export type notification_reason_types =
|
|||
| 'added_you_to_group'
|
||||
| 'you_referred_user'
|
||||
| '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_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
|
||||
// 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'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
|
@ -16,7 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
|
@ -52,6 +53,12 @@ type Referral = {
|
|||
category: 'REFERRAL'
|
||||
}
|
||||
|
||||
type Bonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'UNIQUE_BETTOR_BONUS'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
export type TipTxn = Txn & Tip
|
||||
export type ManalinkTxn = Txn & Manalink
|
||||
|
|
|
@ -57,6 +57,7 @@ export type PrivateUser = {
|
|||
initialIpAddress?: string
|
||||
apiKey?: string
|
||||
notificationPreferences?: notification_subscribe_types
|
||||
lastTimeCheckedBonuses?: number
|
||||
}
|
||||
|
||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||
|
|
|
@ -337,6 +337,20 @@
|
|||
"order": "DESCENDING"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "portfolioHistory",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"order": "ASCENDING"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"order": "ASCENDING"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"fieldOverrides": [
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
plugins: ['lodash'],
|
||||
extends: ['eslint:recommended'],
|
||||
ignorePatterns: ['lib'],
|
||||
ignorePatterns: ['dist', 'lib'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
|
@ -30,6 +30,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
rules: {
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"main": "functions/src/index.js",
|
||||
"dependencies": {
|
||||
"@amplitude/node": "1.10.0",
|
||||
"@google-cloud/functions-framework": "3.1.2",
|
||||
"firebase-admin": "10.0.0",
|
||||
"firebase-functions": "3.21.2",
|
||||
"lodash": "4.17.21",
|
||||
|
|
|
@ -17,7 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object'
|
|||
const firestore = admin.firestore()
|
||||
|
||||
type user_to_reason_texts = {
|
||||
[userId: string]: { reason: notification_reason_types }
|
||||
[userId: string]: { reason: notification_reason_types; isSeeOnHref?: string }
|
||||
}
|
||||
|
||||
export const createNotification = async (
|
||||
|
@ -72,6 +72,7 @@ export const createNotification = async (
|
|||
sourceContractSlug: sourceContract?.slug,
|
||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
||||
isSeenOnHref: userToReasonTexts[userId].isSeeOnHref,
|
||||
}
|
||||
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 userToReasonTexts: user_to_reason_texts = {}
|
||||
// The following functions modify the userToReasonTexts object in place.
|
||||
|
@ -277,6 +298,8 @@ export const createNotification = async (
|
|||
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
||||
} else if (sourceType === 'user' && relatedUserId) {
|
||||
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
|
||||
} else if (sourceType === 'comment' && !sourceContract && relatedUserId) {
|
||||
await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId)
|
||||
}
|
||||
|
||||
// The following functions need sourceContract to be defined.
|
||||
|
@ -309,6 +332,12 @@ export const createNotification = async (
|
|||
})
|
||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||
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
|
||||
}
|
||||
|
|
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-answer'
|
||||
export * from './on-create-bet'
|
||||
export * from './on-create-comment'
|
||||
export * from './on-create-comment-on-contract'
|
||||
export * from './on-view'
|
||||
export * from './unsubscribe'
|
||||
export * from './update-metrics'
|
||||
|
@ -28,6 +28,7 @@ export * from './on-create-liquidity-provision'
|
|||
export * from './on-update-group'
|
||||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
export * from './on-create-comment-on-group'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
@ -38,3 +39,4 @@ export * from './create-contract'
|
|||
export * from './withdraw-liquidity'
|
||||
export * from './create-group'
|
||||
export * from './resolve-market'
|
||||
export * from './get-daily-bonuses'
|
||||
|
|
|
@ -11,7 +11,7 @@ import { createNotification } from './create-notification'
|
|||
|
||||
const firestore = admin.firestore()
|
||||
|
||||
export const onCreateComment = functions
|
||||
export const onCreateCommentOnContract = functions
|
||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||
.firestore.document('contracts/{contractId}/comments/{commentId}')
|
||||
.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
|
||||
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
|
||||
return
|
||||
// TODO: create notification with isSeeOnHref set to the group's /group/questions url
|
||||
|
||||
await firestore
|
||||
.collection('groups')
|
||||
|
|
|
@ -1,96 +1,46 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
import { partition, sumBy } from 'lodash'
|
||||
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { getRedeemableAmount, getRedemptionBets } from '../../common/redeem'
|
||||
|
||||
import { Contract } from '../../common/contract'
|
||||
import { noFees } from '../../common/fees'
|
||||
import { User } from '../../common/user'
|
||||
|
||||
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 contractSnap = await transaction.get(contractDoc)
|
||||
const contractSnap = await trans.get(contractDoc)
|
||||
if (!contractSnap.exists)
|
||||
return { status: 'error', message: 'Invalid contract' }
|
||||
|
||||
const contract = contractSnap.data() as Contract
|
||||
const { mechanism, outcomeType } = contract
|
||||
if (
|
||||
!(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') ||
|
||||
mechanism !== 'cpmm-1'
|
||||
)
|
||||
return { status: 'success' }
|
||||
const { mechanism } = contract
|
||||
if (mechanism !== 'cpmm-1') return { status: 'success' }
|
||||
|
||||
const betsSnap = await transaction.get(
|
||||
firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.where('userId', '==', userId)
|
||||
)
|
||||
const betsColl = firestore.collection(`contracts/${contract.id}/bets`)
|
||||
const betsSnap = await trans.get(betsColl.where('userId', '==', userId))
|
||||
const bets = betsSnap.docs.map((doc) => doc.data() as Bet)
|
||||
const [yesBets, noBets] = partition(bets, (b) => b.outcome === 'YES')
|
||||
const yesShares = sumBy(yesBets, (b) => b.shares)
|
||||
const noShares = sumBy(noBets, (b) => b.shares)
|
||||
|
||||
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 { shares, loanPayment, netAmount } = getRedeemableAmount(bets)
|
||||
if (netAmount === 0) {
|
||||
return { status: 'success' }
|
||||
}
|
||||
const [yesBet, noBet] = getRedemptionBets(shares, loanPayment, contract)
|
||||
|
||||
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' }
|
||||
|
||||
const user = userSnap.data() as User
|
||||
|
||||
const newBalance = user.balance + netAmount
|
||||
|
||||
if (!isFinite(newBalance)) {
|
||||
throw new Error('Invalid user balance for ' + user.username)
|
||||
}
|
||||
|
||||
transaction.update(userDoc, { balance: newBalance })
|
||||
|
||||
transaction.create(yesDoc, yesBet)
|
||||
transaction.create(noDoc, noBet)
|
||||
const yesDoc = betsColl.doc()
|
||||
const noDoc = betsColl.doc()
|
||||
trans.update(userDoc, { balance: newBalance })
|
||||
trans.create(yesDoc, { id: yesDoc.id, userId, ...yesBet })
|
||||
trans.create(noDoc, { id: noDoc.id, userId, ...noBet })
|
||||
|
||||
return { status: 'success' }
|
||||
})
|
||||
|
|
|
@ -46,7 +46,7 @@ export const sellshares = newEndpoint({}, async (req, auth) => {
|
|||
const outcomeBets = userBets.filter((bet) => bet.outcome == outcome)
|
||||
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.`)
|
||||
|
||||
const { newBet, newPool, newP, fees } = getCpmmSellBetInfo(
|
||||
|
|
|
@ -1,138 +1,138 @@
|
|||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { CPMMContract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { subtractObjects } from '../../common/util/object'
|
||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||
import { getUserLiquidityShares } from '../../common/calculate-cpmm'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { noFees } from '../../common/fees'
|
||||
|
||||
import { APIError } from './api'
|
||||
import { redeemShares } from './redeem-shares'
|
||||
|
||||
export const withdrawLiquidity = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
contractId: string
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const { contractId } = data
|
||||
if (!contractId)
|
||||
return { status: 'error', message: 'Missing contract id' }
|
||||
|
||||
return await firestore
|
||||
.runTransaction(async (trans) => {
|
||||
const lpDoc = firestore.doc(`users/${userId}`)
|
||||
const lpSnap = await trans.get(lpDoc)
|
||||
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
|
||||
const lp = lpSnap.data() as User
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const contractSnap = await trans.get(contractDoc)
|
||||
if (!contractSnap.exists)
|
||||
throw new APIError(400, 'Contract not found.')
|
||||
const contract = contractSnap.data() as CPMMContract
|
||||
|
||||
const liquidityCollection = firestore.collection(
|
||||
`contracts/${contractId}/liquidity`
|
||||
)
|
||||
|
||||
const liquiditiesSnap = await trans.get(liquidityCollection)
|
||||
|
||||
const liquidities = liquiditiesSnap.docs.map(
|
||||
(doc) => doc.data() as LiquidityProvision
|
||||
)
|
||||
|
||||
const userShares = getUserLiquidityShares(
|
||||
userId,
|
||||
contract,
|
||||
liquidities
|
||||
)
|
||||
|
||||
// zero all added amounts for now
|
||||
// can add support for partial withdrawals in the future
|
||||
liquiditiesSnap.docs
|
||||
.filter(
|
||||
(_, i) =>
|
||||
!liquidities[i].isAnte && liquidities[i].userId === userId
|
||||
)
|
||||
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
|
||||
|
||||
const payout = Math.min(...Object.values(userShares))
|
||||
if (payout <= 0) return {}
|
||||
|
||||
const newBalance = lp.balance + payout
|
||||
const newTotalDeposits = lp.totalDeposits + payout
|
||||
trans.update(lpDoc, {
|
||||
balance: newBalance,
|
||||
totalDeposits: newTotalDeposits,
|
||||
} as Partial<User>)
|
||||
|
||||
const newPool = subtractObjects(contract.pool, userShares)
|
||||
|
||||
const minPoolShares = Math.min(...Object.values(newPool))
|
||||
const adjustedTotal = contract.totalLiquidity - payout
|
||||
|
||||
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
|
||||
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
|
||||
|
||||
trans.update(contractDoc, {
|
||||
pool: newPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
})
|
||||
|
||||
const prob = getProbability(contract)
|
||||
|
||||
// surplus shares become user's bets
|
||||
const bets = Object.entries(userShares)
|
||||
.map(([outcome, shares]) =>
|
||||
shares - payout < 1 // don't create bet if less than 1 share
|
||||
? undefined
|
||||
: ({
|
||||
userId: userId,
|
||||
contractId: contract.id,
|
||||
amount:
|
||||
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
|
||||
shares: shares - payout,
|
||||
outcome,
|
||||
probBefore: prob,
|
||||
probAfter: prob,
|
||||
createdTime: Date.now(),
|
||||
isLiquidityProvision: true,
|
||||
fees: noFees,
|
||||
} as Omit<Bet, 'id'>)
|
||||
)
|
||||
.filter((x) => x !== undefined)
|
||||
|
||||
for (const bet of bets) {
|
||||
const doc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
trans.create(doc, { id: doc.id, ...bet })
|
||||
}
|
||||
|
||||
return userShares
|
||||
})
|
||||
.then(async (result) => {
|
||||
// redeem surplus bet with pre-existing bets
|
||||
await redeemShares(userId, contractId)
|
||||
|
||||
console.log('userid', userId, 'withdraws', result)
|
||||
return { status: 'success', userShares: result }
|
||||
})
|
||||
.catch((e) => {
|
||||
return { status: 'error', message: e.message }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const firestore = admin.firestore()
|
||||
import * as functions from 'firebase-functions'
|
||||
import * as admin from 'firebase-admin'
|
||||
|
||||
import { CPMMContract } from '../../common/contract'
|
||||
import { User } from '../../common/user'
|
||||
import { subtractObjects } from '../../common/util/object'
|
||||
import { LiquidityProvision } from '../../common/liquidity-provision'
|
||||
import { getUserLiquidityShares } from '../../common/calculate-cpmm'
|
||||
import { Bet } from '../../common/bet'
|
||||
import { getProbability } from '../../common/calculate'
|
||||
import { noFees } from '../../common/fees'
|
||||
|
||||
import { APIError } from './api'
|
||||
import { redeemShares } from './redeem-shares'
|
||||
|
||||
export const withdrawLiquidity = functions
|
||||
.runWith({ minInstances: 1 })
|
||||
.https.onCall(
|
||||
async (
|
||||
data: {
|
||||
contractId: string
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const userId = context?.auth?.uid
|
||||
if (!userId) return { status: 'error', message: 'Not authorized' }
|
||||
|
||||
const { contractId } = data
|
||||
if (!contractId)
|
||||
return { status: 'error', message: 'Missing contract id' }
|
||||
|
||||
return await firestore
|
||||
.runTransaction(async (trans) => {
|
||||
const lpDoc = firestore.doc(`users/${userId}`)
|
||||
const lpSnap = await trans.get(lpDoc)
|
||||
if (!lpSnap.exists) throw new APIError(400, 'User not found.')
|
||||
const lp = lpSnap.data() as User
|
||||
|
||||
const contractDoc = firestore.doc(`contracts/${contractId}`)
|
||||
const contractSnap = await trans.get(contractDoc)
|
||||
if (!contractSnap.exists)
|
||||
throw new APIError(400, 'Contract not found.')
|
||||
const contract = contractSnap.data() as CPMMContract
|
||||
|
||||
const liquidityCollection = firestore.collection(
|
||||
`contracts/${contractId}/liquidity`
|
||||
)
|
||||
|
||||
const liquiditiesSnap = await trans.get(liquidityCollection)
|
||||
|
||||
const liquidities = liquiditiesSnap.docs.map(
|
||||
(doc) => doc.data() as LiquidityProvision
|
||||
)
|
||||
|
||||
const userShares = getUserLiquidityShares(
|
||||
userId,
|
||||
contract,
|
||||
liquidities
|
||||
)
|
||||
|
||||
// zero all added amounts for now
|
||||
// can add support for partial withdrawals in the future
|
||||
liquiditiesSnap.docs
|
||||
.filter(
|
||||
(_, i) =>
|
||||
!liquidities[i].isAnte && liquidities[i].userId === userId
|
||||
)
|
||||
.forEach((doc) => trans.update(doc.ref, { amount: 0 }))
|
||||
|
||||
const payout = Math.min(...Object.values(userShares))
|
||||
if (payout <= 0) return {}
|
||||
|
||||
const newBalance = lp.balance + payout
|
||||
const newTotalDeposits = lp.totalDeposits + payout
|
||||
trans.update(lpDoc, {
|
||||
balance: newBalance,
|
||||
totalDeposits: newTotalDeposits,
|
||||
} as Partial<User>)
|
||||
|
||||
const newPool = subtractObjects(contract.pool, userShares)
|
||||
|
||||
const minPoolShares = Math.min(...Object.values(newPool))
|
||||
const adjustedTotal = contract.totalLiquidity - payout
|
||||
|
||||
// total liquidity is a bogus number; use minPoolShares to prevent from going negative
|
||||
const newTotalLiquidity = Math.max(adjustedTotal, minPoolShares)
|
||||
|
||||
trans.update(contractDoc, {
|
||||
pool: newPool,
|
||||
totalLiquidity: newTotalLiquidity,
|
||||
})
|
||||
|
||||
const prob = getProbability(contract)
|
||||
|
||||
// surplus shares become user's bets
|
||||
const bets = Object.entries(userShares)
|
||||
.map(([outcome, shares]) =>
|
||||
shares - payout < 1 // don't create bet if less than 1 share
|
||||
? undefined
|
||||
: ({
|
||||
userId: userId,
|
||||
contractId: contract.id,
|
||||
amount:
|
||||
(outcome === 'YES' ? prob : 1 - prob) * (shares - payout),
|
||||
shares: shares - payout,
|
||||
outcome,
|
||||
probBefore: prob,
|
||||
probAfter: prob,
|
||||
createdTime: Date.now(),
|
||||
isLiquidityProvision: true,
|
||||
fees: noFees,
|
||||
} as Omit<Bet, 'id'>)
|
||||
)
|
||||
.filter((x) => x !== undefined)
|
||||
|
||||
for (const bet of bets) {
|
||||
const doc = firestore
|
||||
.collection(`contracts/${contract.id}/bets`)
|
||||
.doc()
|
||||
trans.create(doc, { id: doc.id, ...bet })
|
||||
}
|
||||
|
||||
return userShares
|
||||
})
|
||||
.then(async (result) => {
|
||||
// redeem surplus bet with pre-existing bets
|
||||
await redeemShares(userId, contractId)
|
||||
|
||||
console.log('userid', userId, 'withdraws', result)
|
||||
return { status: 'success', userShares: result }
|
||||
})
|
||||
.catch((e) => {
|
||||
return { status: 'error', message: e.message }
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const firestore = admin.firestore()
|
||||
|
|
|
@ -19,6 +19,7 @@ module.exports = {
|
|||
],
|
||||
'@next/next/no-img-element': 'off',
|
||||
'@next/next/no-typos': 'off',
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
},
|
||||
env: {
|
||||
|
|
|
@ -21,6 +21,7 @@ import { Title } from '../title'
|
|||
import { TweetButton } from '../tweet-button'
|
||||
import { InfoTooltip } from '../info-tooltip'
|
||||
import { TagsInput } from 'web/components/tags-input'
|
||||
import { DuplicateContractButton } from '../copy-contract-button'
|
||||
|
||||
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'
|
||||
|
@ -71,6 +72,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
tweetText={getTweetText(contract, false)}
|
||||
/>
|
||||
<ShareEmbedButton contract={contract} toastClassName={'-left-20'} />
|
||||
<DuplicateContractButton contract={contract} />
|
||||
</Row>
|
||||
<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 (
|
||||
!selectedUsers.map((user) => user.name).includes(user.name) &&
|
||||
!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 { ProfileSummary } from './profile-menu'
|
||||
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 { CreateQuestionButton } from 'web/components/create-question-button'
|
||||
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 { Group } from 'common/group'
|
||||
import { Spacer } from '../layout/spacer'
|
||||
import { usePreferredNotifications } from 'web/hooks/use-notifications'
|
||||
import { setNotificationsAsSeen } from 'web/pages/notifications'
|
||||
|
||||
function getNavigation() {
|
||||
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>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
|
@ -237,14 +243,36 @@ export default function Sidebar(props: { className?: string }) {
|
|||
<div className="h-[1px] bg-gray-300" />
|
||||
</div>
|
||||
)}
|
||||
<GroupsList currentPage={currentPage} memberItems={memberItems} />
|
||||
<GroupsList
|
||||
currentPage={router.asPath}
|
||||
memberItems={memberItems}
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
||||
const { currentPage, memberItems } = props
|
||||
function GroupsList(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 (
|
||||
<>
|
||||
<SidebarItem
|
||||
|
@ -257,9 +285,14 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
|
|||
<a
|
||||
key={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>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -2,17 +2,29 @@ import { BellIcon } from '@heroicons/react/outline'
|
|||
import clsx from 'clsx'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
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 { 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 }) {
|
||||
const user = useUser()
|
||||
const notifications = usePreferredGroupedNotifications(user?.id, {
|
||||
const privateUser = usePrivateUser(user?.id)
|
||||
const notifications = usePreferredGroupedNotifications(privateUser?.id, {
|
||||
unseenOnly: true,
|
||||
})
|
||||
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()
|
||||
useEffect(() => {
|
||||
if (router.pathname.endsWith('notifications')) return setSeen(true)
|
||||
|
@ -24,7 +36,9 @@ export default function NotificationsIcon(props: { className?: string }) {
|
|||
<div className={'relative'}>
|
||||
{!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">
|
||||
{notifications.length}
|
||||
{notifications.length > NOTIFICATIONS_PER_PAGE
|
||||
? `${NOTIFICATIONS_PER_PAGE}+`
|
||||
: notifications.length}
|
||||
</div>
|
||||
)}
|
||||
<BellIcon className={clsx(props.className)} />
|
||||
|
|
|
@ -7,9 +7,10 @@ import { groupBy, map } from 'lodash'
|
|||
|
||||
export type NotificationGroup = {
|
||||
notifications: Notification[]
|
||||
sourceContractId: string
|
||||
groupedById: string
|
||||
isSeen: boolean
|
||||
timePeriod: string
|
||||
type: 'income' | 'normal'
|
||||
}
|
||||
|
||||
export function usePreferredGroupedNotifications(
|
||||
|
@ -37,25 +38,43 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
new Date(notification.createdTime).toDateString()
|
||||
)
|
||||
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(
|
||||
notificationGroupsByDay[day],
|
||||
normalNotificationsGroupedByDay,
|
||||
(notification) => {
|
||||
return notification.sourceContractId
|
||||
}
|
||||
)
|
||||
notificationGroups = notificationGroups.concat(
|
||||
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
|
||||
const notificationGroup: NotificationGroup = {
|
||||
notifications: groupedNotificationsByContractId[contractId].sort(
|
||||
(a, b) => {
|
||||
return b.createdTime - a.createdTime
|
||||
}
|
||||
),
|
||||
sourceContractId: contractId,
|
||||
isSeen: groupedNotificationsByContractId[contractId][0].isSeen,
|
||||
notifications: notificationsForContractId,
|
||||
groupedById: contractId,
|
||||
isSeen: notificationsForContractId[0].isSeen,
|
||||
timePeriod: day,
|
||||
type: 'normal',
|
||||
}
|
||||
return notificationGroup
|
||||
})
|
||||
|
@ -64,11 +83,11 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
return notificationGroups
|
||||
}
|
||||
|
||||
function usePreferredNotifications(
|
||||
export function usePreferredNotifications(
|
||||
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 [notifications, setNotifications] = useState<Notification[]>([])
|
||||
const [userAppropriateNotifications, setUserAppropriateNotifications] =
|
||||
|
@ -93,9 +112,11 @@ function usePreferredNotifications(
|
|||
const notificationsToShow = getAppropriateNotifications(
|
||||
notifications,
|
||||
privateUser.notificationPreferences
|
||||
).filter((n) =>
|
||||
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
|
||||
)
|
||||
setUserAppropriateNotifications(notificationsToShow)
|
||||
}, [privateUser, notifications])
|
||||
}, [privateUser, notifications, customHref])
|
||||
|
||||
return userAppropriateNotifications
|
||||
}
|
||||
|
|
|
@ -73,3 +73,7 @@ export function sellBet(params: any) {
|
|||
export function createGroup(params: any) {
|
||||
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 { User } from 'common/user'
|
||||
|
||||
export default function Create() {
|
||||
const [question, setQuestion] = useState('')
|
||||
// get query params:
|
||||
const router = useRouter()
|
||||
const { groupId } = router.query as { groupId: string }
|
||||
useTracking('view create page')
|
||||
const creator = useUser()
|
||||
type NewQuestionParams = {
|
||||
groupId?: string
|
||||
q: string
|
||||
type: string
|
||||
description: string
|
||||
closeTime: string
|
||||
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(() => {
|
||||
if (creator === null) router.push('/')
|
||||
}, [creator, router])
|
||||
|
@ -65,11 +83,7 @@ export default function Create() {
|
|||
</div>
|
||||
</form>
|
||||
<Spacer h={6} />
|
||||
<NewContract
|
||||
question={question}
|
||||
groupId={groupId}
|
||||
creator={creator}
|
||||
/>
|
||||
<NewContract question={question} params={params} creator={creator} />
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
|
@ -80,20 +94,21 @@ export default function Create() {
|
|||
export function NewContract(props: {
|
||||
creator: User
|
||||
question: string
|
||||
groupId?: string
|
||||
params?: NewQuestionParams
|
||||
}) {
|
||||
const { creator, question, groupId } = props
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
|
||||
const { creator, question, params } = props
|
||||
const { groupId, initValue } = params ?? {}
|
||||
const [outcomeType, setOutcomeType] = useState<outcomeType>(
|
||||
(params?.outcomeType as outcomeType) ?? 'BINARY'
|
||||
)
|
||||
const [initialProb] = useState(50)
|
||||
|
||||
const [minString, setMinString] = useState('')
|
||||
const [maxString, setMaxString] = useState('')
|
||||
const [isLogScale, setIsLogScale] = useState(false)
|
||||
const [initialValueString, setInitialValueString] = useState('')
|
||||
const [minString, setMinString] = useState(params?.min ?? '')
|
||||
const [maxString, setMaxString] = useState(params?.max ?? '')
|
||||
const [isLogScale, setIsLogScale] = useState<boolean>(!!params?.isLogScale)
|
||||
const [initialValueString, setInitialValueString] = useState(initValue)
|
||||
|
||||
const [description, setDescription] = useState('')
|
||||
// const [tagText, setTagText] = useState<string>(tag ?? '')
|
||||
// const tags = parseWordsAsTags(tagText)
|
||||
const [description, setDescription] = useState(params?.description ?? '')
|
||||
useEffect(() => {
|
||||
if (groupId && creator)
|
||||
getGroup(groupId).then((group) => {
|
||||
|
@ -105,18 +120,17 @@ export function NewContract(props: {
|
|||
}, [creator, groupId])
|
||||
const [ante, _setAnte] = useState(FIXED_ANTE)
|
||||
|
||||
// useEffect(() => {
|
||||
// if (ante === null && creator) {
|
||||
// const initialAnte = creator.balance < 100 ? MINIMUM_ANTE : 100
|
||||
// setAnte(initialAnte)
|
||||
// }
|
||||
// }, [ante, creator])
|
||||
|
||||
// const [anteError, setAnteError] = useState<string | undefined>()
|
||||
// If params.closeTime is set, extract out the specified date and time
|
||||
// By default, close the market a week from today
|
||||
const weekFromToday = dayjs().add(7, 'day').format('YYYY-MM-DD')
|
||||
const [closeDate, setCloseDate] = useState<undefined | string>(weekFromToday)
|
||||
const [closeHoursMinutes, setCloseHoursMinutes] = useState<string>('23:59')
|
||||
const timeInMs = Number(params?.closeTime ?? 0)
|
||||
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 [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [selectedGroup, setSelectedGroup] = useState<Group | undefined>(
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
Notification,
|
||||
notification_reason_types,
|
||||
notification_source_types,
|
||||
notification_source_update_types,
|
||||
} from 'common/notification'
|
||||
import { Notification } from 'common/notification'
|
||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
import { Page } from 'web/components/page'
|
||||
|
@ -31,47 +26,40 @@ import {
|
|||
ProbPercentLabel,
|
||||
} from 'web/components/outcome-label'
|
||||
import {
|
||||
groupNotifications,
|
||||
NotificationGroup,
|
||||
usePreferredGroupedNotifications,
|
||||
} 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 { formatMoney } from 'common/util/format'
|
||||
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() {
|
||||
const user = useUser()
|
||||
const [unseenNotificationGroups, setUnseenNotificationGroups] = useState<
|
||||
NotificationGroup[] | undefined
|
||||
>(undefined)
|
||||
const allNotificationGroups = usePreferredGroupedNotifications(user?.id, {
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const groupedNotifications = usePreferredGroupedNotifications(user?.id, {
|
||||
unseenOnly: false,
|
||||
})
|
||||
|
||||
const [paginatedNotificationGroups, setPaginatedNotificationGroups] =
|
||||
useState<NotificationGroup[]>([])
|
||||
useEffect(() => {
|
||||
if (!allNotificationGroups) return
|
||||
// Don't re-add notifications that are visible right now or have been seen already.
|
||||
const currentlyVisibleUnseenNotificationIds = Object.values(
|
||||
unseenNotificationGroups ?? []
|
||||
)
|
||||
.map((n) => n.notifications.map((n) => n.id))
|
||||
.flat()
|
||||
const unseenGroupedNotifications = groupNotifications(
|
||||
allNotificationGroups
|
||||
.map((notification: NotificationGroup) => notification.notifications)
|
||||
.flat()
|
||||
.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 (!groupedNotifications) return
|
||||
const start = (page - 1) * NOTIFICATIONS_PER_PAGE
|
||||
const end = start + NOTIFICATIONS_PER_PAGE
|
||||
const maxNotificationsToShow = groupedNotifications.slice(start, end)
|
||||
const remainingNotification = groupedNotifications.slice(end)
|
||||
for (const notification of remainingNotification) {
|
||||
if (notification.isSeen) break
|
||||
else setNotificationsAsSeen(notification.notifications)
|
||||
}
|
||||
setPaginatedNotificationGroups(maxNotificationsToShow)
|
||||
}, [groupedNotifications, page])
|
||||
|
||||
if (user === undefined) {
|
||||
return <LoadingIndicator />
|
||||
|
@ -80,7 +68,6 @@ export default function Notifications() {
|
|||
return <Custom404 />
|
||||
}
|
||||
|
||||
// TODO: use infinite scroll
|
||||
return (
|
||||
<Page>
|
||||
<div className={'p-2 sm:p-4'}>
|
||||
|
@ -90,53 +77,74 @@ export default function Notifications() {
|
|||
defaultIndex={0}
|
||||
tabs={[
|
||||
{
|
||||
title: 'New Notifications',
|
||||
content: unseenNotificationGroups ? (
|
||||
title: 'Notifications',
|
||||
content: groupedNotifications ? (
|
||||
<div className={''}>
|
||||
{unseenNotificationGroups.length === 0 &&
|
||||
"You don't have any new notifications."}
|
||||
{unseenNotificationGroups.map((notification) =>
|
||||
{paginatedNotificationGroups.length === 0 &&
|
||||
"You don't have any notifications. Try changing your settings to see more."}
|
||||
{paginatedNotificationGroups.map((notification) =>
|
||||
notification.notifications.length === 1 ? (
|
||||
<NotificationItem
|
||||
notification={notification.notifications[0]}
|
||||
key={notification.notifications[0].id}
|
||||
/>
|
||||
) : notification.type === 'income' ? (
|
||||
<IncomeNotificationGroupItem
|
||||
notificationGroup={notification}
|
||||
key={notification.groupedById + notification.timePeriod}
|
||||
/>
|
||||
) : (
|
||||
<NotificationGroupItem
|
||||
notificationGroup={notification}
|
||||
key={
|
||||
notification.sourceContractId +
|
||||
notification.timePeriod
|
||||
}
|
||||
key={notification.groupedById + notification.timePeriod}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'All Notifications',
|
||||
content: allNotificationGroups ? (
|
||||
<div className={''}>
|
||||
{allNotificationGroups.length === 0 &&
|
||||
"You don't have any notifications. Try changing your settings to see more."}
|
||||
{allNotificationGroups.map((notification) =>
|
||||
notification.notifications.length === 1 ? (
|
||||
<NotificationItem
|
||||
notification={notification.notifications[0]}
|
||||
key={notification.notifications[0].id}
|
||||
/>
|
||||
) : (
|
||||
<NotificationGroupItem
|
||||
notificationGroup={notification}
|
||||
key={
|
||||
notification.sourceContractId +
|
||||
notification.timePeriod
|
||||
}
|
||||
/>
|
||||
)
|
||||
{groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
|
||||
<nav
|
||||
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">
|
||||
<p className="text-sm text-gray-700">
|
||||
Showing{' '}
|
||||
<span className="font-medium">
|
||||
{page === 1
|
||||
? page
|
||||
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="font-medium">
|
||||
{page * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
of{' '}
|
||||
<span className="font-medium">
|
||||
{groupedNotifications.length}
|
||||
</span>{' '}
|
||||
results
|
||||
</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>
|
||||
) : (
|
||||
|
@ -158,13 +166,12 @@ export default function Notifications() {
|
|||
)
|
||||
}
|
||||
|
||||
const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||
export const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||
notifications.forEach((notification) => {
|
||||
if (!notification.isSeen)
|
||||
updateDoc(
|
||||
doc(db, `users/${notification.userId}/notifications/`, notification.id),
|
||||
{
|
||||
...notification,
|
||||
isSeen: true,
|
||||
viewTime: new Date(),
|
||||
}
|
||||
|
@ -173,6 +180,152 @@ const setNotificationsAsSeen = (notifications: Notification[]) => {
|
|||
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: {
|
||||
notificationGroup: NotificationGroup
|
||||
className?: string
|
||||
|
@ -187,17 +340,28 @@ function NotificationGroupItem(props: {
|
|||
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])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
||||
className,
|
||||
!expanded ? 'hover:bg-gray-100' : ''
|
||||
!expanded ? 'hover:bg-gray-100' : '',
|
||||
highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : ''
|
||||
)}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
|
@ -432,7 +596,7 @@ function NotificationSettings() {
|
|||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Referral bonuses you've received"}
|
||||
label={"Income & referral bonuses you've received"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
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: {
|
||||
notification: Notification
|
||||
justSummary?: boolean
|
||||
|
@ -522,6 +675,16 @@ function NotificationItem(props: {
|
|||
}
|
||||
}, [reasonText, sourceText])
|
||||
|
||||
const [highlighted, setHighlighted] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!notification.isSeen) {
|
||||
setHighlighted(true)
|
||||
setTimeout(() => {
|
||||
setHighlighted(false)
|
||||
}, HIGHLIGHT_DURATION)
|
||||
}
|
||||
}, [notification.isSeen])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsAsSeen([notification])
|
||||
}, [notification])
|
||||
|
@ -559,22 +722,21 @@ function NotificationItem(props: {
|
|||
<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={'flex pl-1 sm:pl-0'}>
|
||||
<UserLink
|
||||
name={sourceUserName || ''}
|
||||
username={sourceUserUsername || ''}
|
||||
className={'mr-0 flex-shrink-0'}
|
||||
/>
|
||||
{sourceType != 'bonus' && (
|
||||
<UserLink
|
||||
name={sourceUserName || ''}
|
||||
username={sourceUserUsername || ''}
|
||||
className={'mr-0 flex-shrink-0'}
|
||||
/>
|
||||
)}
|
||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||
<span className={'flex-shrink-0'}>
|
||||
{sourceType &&
|
||||
reason &&
|
||||
getReasonForShowingNotification(
|
||||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
undefined,
|
||||
true
|
||||
).replace(' on', '')}
|
||||
getReasonForShowingNotification(notification, true).replace(
|
||||
' on',
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
<div className={'ml-1 text-black'}>
|
||||
<NotificationTextLabel
|
||||
|
@ -593,37 +755,41 @@ function NotificationItem(props: {
|
|||
}
|
||||
|
||||
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()}>
|
||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||
<Avatar
|
||||
avatarUrl={sourceUserAvatarUrl}
|
||||
size={'sm'}
|
||||
className={'mr-2'}
|
||||
username={sourceUserName}
|
||||
/>
|
||||
{sourceType != 'bonus' ? (
|
||||
<Avatar
|
||||
avatarUrl={sourceUserAvatarUrl}
|
||||
size={'sm'}
|
||||
className={'mr-2'}
|
||||
username={sourceUserName}
|
||||
/>
|
||||
) : (
|
||||
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||
)}
|
||||
<div className={'flex-1 overflow-hidden sm:flex'}>
|
||||
<div
|
||||
className={
|
||||
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
||||
}
|
||||
>
|
||||
<UserLink
|
||||
name={sourceUserName || ''}
|
||||
username={sourceUserUsername || ''}
|
||||
className={'mr-0 flex-shrink-0'}
|
||||
/>
|
||||
{sourceType != 'bonus' && sourceUpdateType != 'closed' && (
|
||||
<UserLink
|
||||
name={sourceUserName || ''}
|
||||
username={sourceUserUsername || ''}
|
||||
className={'mr-0 flex-shrink-0'}
|
||||
/>
|
||||
)}
|
||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||
{sourceType && reason && (
|
||||
<div className={'inline truncate'}>
|
||||
{getReasonForShowingNotification(
|
||||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
undefined,
|
||||
false,
|
||||
sourceSlug
|
||||
)}
|
||||
{getReasonForShowingNotification(notification, false)}
|
||||
<a
|
||||
href={
|
||||
sourceContractCreatorUsername
|
||||
|
@ -684,13 +850,7 @@ function NotificationTextLabel(props: {
|
|||
return <span>{contract?.question || sourceContractTitle}</span>
|
||||
if (!sourceText) return <div />
|
||||
// Resolved contracts
|
||||
if (
|
||||
isNotificationAboutContractResolution(
|
||||
sourceType,
|
||||
sourceUpdateType,
|
||||
contract
|
||||
)
|
||||
) {
|
||||
if (sourceType === 'contract' && sourceUpdateType === 'resolved') {
|
||||
{
|
||||
if (sourceText === 'YES' || sourceText == 'NO') {
|
||||
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
||||
|
@ -730,6 +890,12 @@ function NotificationTextLabel(props: {
|
|||
return (
|
||||
<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 (
|
||||
|
@ -740,15 +906,13 @@ function NotificationTextLabel(props: {
|
|||
}
|
||||
|
||||
function getReasonForShowingNotification(
|
||||
source: notification_source_types,
|
||||
reason: notification_reason_types,
|
||||
sourceUpdateType: notification_source_update_types | undefined,
|
||||
contract: Contract | undefined | null,
|
||||
simple?: boolean,
|
||||
sourceSlug?: string
|
||||
notification: Notification,
|
||||
simple?: boolean
|
||||
) {
|
||||
const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } =
|
||||
notification
|
||||
let reasonText: string
|
||||
switch (source) {
|
||||
switch (sourceType) {
|
||||
case 'comment':
|
||||
if (reason === 'reply_to_users_answer')
|
||||
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
||||
|
@ -768,16 +932,9 @@ function getReasonForShowingNotification(
|
|||
break
|
||||
case 'contract':
|
||||
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
||||
else if (
|
||||
isNotificationAboutContractResolution(
|
||||
source,
|
||||
sourceUpdateType,
|
||||
contract
|
||||
)
|
||||
)
|
||||
reasonText = `resolved`
|
||||
else if (sourceUpdateType === 'resolved') reasonText = `resolved`
|
||||
else if (sourceUpdateType === 'closed')
|
||||
reasonText = `please resolve your question`
|
||||
reasonText = `Please resolve your question`
|
||||
else reasonText = `updated`
|
||||
break
|
||||
case 'answer':
|
||||
|
@ -805,6 +962,15 @@ function getReasonForShowingNotification(
|
|||
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||
else reasonText = 'joined because of you'
|
||||
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:
|
||||
reasonText = ''
|
||||
}
|
||||
|
|
208
yarn.lock
208
yarn.lock
|
@ -2181,6 +2181,20 @@
|
|||
google-gax "^2.24.1"
|
||||
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":
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b"
|
||||
|
@ -2926,7 +2940,7 @@
|
|||
"@types/qs" "*"
|
||||
"@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"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034"
|
||||
integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==
|
||||
|
@ -3049,6 +3063,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
|
||||
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":
|
||||
version "4.0.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
|
||||
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
|
||||
|
@ -3750,6 +3769,11 @@ autoprefixer@^10.3.7, autoprefixer@^10.4.2:
|
|||
picocolors "^1.0.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:
|
||||
version "4.4.2"
|
||||
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"
|
||||
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"
|
||||
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==
|
||||
|
@ -4236,6 +4260,16 @@ clone-response@^1.0.2:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
|
@ -5277,7 +5311,7 @@ error-ex@^1.3.1:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814"
|
||||
integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==
|
||||
|
@ -5657,7 +5691,7 @@ execa@^5.0.0:
|
|||
signal-exit "^3.0.3"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf"
|
||||
integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==
|
||||
|
@ -5871,7 +5905,7 @@ find-up@^3.0.0:
|
|||
dependencies:
|
||||
locate-path "^3.0.0"
|
||||
|
||||
find-up@^4.0.0:
|
||||
find-up@^4.0.0, find-up@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
|
||||
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"
|
||||
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:
|
||||
version "6.5.2"
|
||||
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:
|
||||
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:
|
||||
version "2.1.6"
|
||||
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-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:
|
||||
version "0.2.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
|
||||
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
|
||||
|
@ -6989,7 +7043,7 @@ is-ci@^2.0.0:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69"
|
||||
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"
|
||||
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:
|
||||
version "4.0.3"
|
||||
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:
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
@ -8126,6 +8198,16 @@ nopt@1.0.10:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
||||
|
||||
on-finished@2.4.1:
|
||||
on-finished@2.4.1, on-finished@^2.3.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
|
@ -9463,6 +9545,25 @@ react@17.0.2, react@^17.0.1:
|
|||
loose-envify "^1.1.0"
|
||||
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:
|
||||
version "1.1.14"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0-next.3"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
@ -9959,16 +10069,16 @@ semver-diff@^3.1.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||
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:
|
||||
version "6.3.0"
|
||||
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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.13.0"
|
||||
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"
|
||||
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:
|
||||
version "0.4.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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-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:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
|
|
Loading…
Reference in New Issue
Block a user