betting streaks (#777)
* Parse notif, show betting streaks modal, schedule function * Ignore streaks of 0 * Pass notifyFills the contract * Turn 9am into a constant * Lint * Up streak reward, fix timing logic * Change wording
This commit is contained in:
parent
4f3202f90b
commit
00c9fa61c3
|
@ -38,6 +38,7 @@ export type notification_source_types =
|
||||||
| 'user'
|
| 'user'
|
||||||
| 'bonus'
|
| 'bonus'
|
||||||
| 'challenge'
|
| 'challenge'
|
||||||
|
| 'betting_streak_bonus'
|
||||||
|
|
||||||
export type notification_source_update_types =
|
export type notification_source_update_types =
|
||||||
| 'created'
|
| 'created'
|
||||||
|
@ -66,3 +67,4 @@ export type notification_reason_types =
|
||||||
| 'bet_fill'
|
| 'bet_fill'
|
||||||
| 'user_joined_from_your_group_invite'
|
| 'user_joined_from_your_group_invite'
|
||||||
| 'challenge_accepted'
|
| 'challenge_accepted'
|
||||||
|
| 'betting_streak_incremented'
|
||||||
|
|
|
@ -4,3 +4,5 @@ export const NUMERIC_FIXED_VAR = 0.005
|
||||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||||
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
export const UNIQUE_BETTOR_BONUS_AMOUNT = 10
|
||||||
|
export const BETTING_STREAK_BONUS_AMOUNT = 5
|
||||||
|
export const BETTING_STREAK_RESET_HOUR = 9
|
||||||
|
|
|
@ -16,7 +16,13 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
amount: number
|
amount: number
|
||||||
token: 'M$' // | 'USD' | MarketOutcome
|
token: 'M$' // | 'USD' | MarketOutcome
|
||||||
|
|
||||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
category:
|
||||||
|
| 'CHARITY'
|
||||||
|
| 'MANALINK'
|
||||||
|
| 'TIP'
|
||||||
|
| 'REFERRAL'
|
||||||
|
| 'UNIQUE_BETTOR_BONUS'
|
||||||
|
| 'BETTING_STREAK_BONUS'
|
||||||
|
|
||||||
// Any extra data
|
// Any extra data
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
@ -57,7 +63,7 @@ type Referral = {
|
||||||
type Bonus = {
|
type Bonus = {
|
||||||
fromType: 'BANK'
|
fromType: 'BANK'
|
||||||
toType: 'USER'
|
toType: 'USER'
|
||||||
category: 'UNIQUE_BETTOR_BONUS'
|
category: 'UNIQUE_BETTOR_BONUS' | 'BETTING_STREAK_BONUS'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DonationTxn = Txn & Donation
|
export type DonationTxn = Txn & Donation
|
||||||
|
|
|
@ -41,6 +41,8 @@ export type User = {
|
||||||
referredByGroupId?: string
|
referredByGroupId?: string
|
||||||
lastPingTime?: number
|
lastPingTime?: number
|
||||||
shouldShowWelcome?: boolean
|
shouldShowWelcome?: boolean
|
||||||
|
lastBetTime?: number
|
||||||
|
currentBettingStreak?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
|
||||||
|
|
|
@ -504,3 +504,38 @@ export const createChallengeAcceptedNotification = async (
|
||||||
}
|
}
|
||||||
return await notificationRef.set(removeUndefinedProps(notification))
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createBettingStreakBonusNotification = async (
|
||||||
|
user: User,
|
||||||
|
txnId: string,
|
||||||
|
bet: Bet,
|
||||||
|
contract: Contract,
|
||||||
|
amount: number,
|
||||||
|
idempotencyKey: string
|
||||||
|
) => {
|
||||||
|
const notificationRef = firestore
|
||||||
|
.collection(`/users/${user.id}/notifications`)
|
||||||
|
.doc(idempotencyKey)
|
||||||
|
const notification: Notification = {
|
||||||
|
id: idempotencyKey,
|
||||||
|
userId: user.id,
|
||||||
|
reason: 'betting_streak_incremented',
|
||||||
|
createdTime: Date.now(),
|
||||||
|
isSeen: false,
|
||||||
|
sourceId: txnId,
|
||||||
|
sourceType: 'betting_streak_bonus',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceUserName: user.name,
|
||||||
|
sourceUserUsername: user.username,
|
||||||
|
sourceUserAvatarUrl: user.avatarUrl,
|
||||||
|
sourceText: amount.toString(),
|
||||||
|
sourceSlug: `/${contract.creatorUsername}/${contract.slug}/bets/${bet.id}`,
|
||||||
|
sourceTitle: 'Betting Streak Bonus',
|
||||||
|
// Perhaps not necessary, but just in case
|
||||||
|
sourceContractSlug: contract.slug,
|
||||||
|
sourceContractId: contract.id,
|
||||||
|
sourceContractTitle: contract.question,
|
||||||
|
sourceContractCreatorUsername: contract.creatorUsername,
|
||||||
|
}
|
||||||
|
return await notificationRef.set(removeUndefinedProps(notification))
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export * from './on-create-comment-on-group'
|
||||||
export * from './on-create-txn'
|
export * from './on-create-txn'
|
||||||
export * from './on-delete-group'
|
export * from './on-delete-group'
|
||||||
export * from './score-contracts'
|
export * from './score-contracts'
|
||||||
|
export * from './reset-betting-streaks'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
|
|
@ -3,15 +3,20 @@ import * as admin from 'firebase-admin'
|
||||||
import { keyBy, uniq } from 'lodash'
|
import { keyBy, uniq } from 'lodash'
|
||||||
|
|
||||||
import { Bet, LimitBet } from '../../common/bet'
|
import { Bet, LimitBet } from '../../common/bet'
|
||||||
import { getContract, getUser, getValues, isProd, log } from './utils'
|
import { getUser, getValues, isProd, log } from './utils'
|
||||||
import {
|
import {
|
||||||
createBetFillNotification,
|
createBetFillNotification,
|
||||||
|
createBettingStreakBonusNotification,
|
||||||
createNotification,
|
createNotification,
|
||||||
} from './create-notification'
|
} from './create-notification'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { runTxn, TxnData } from './transact'
|
import { runTxn, TxnData } from './transact'
|
||||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
import {
|
||||||
|
BETTING_STREAK_BONUS_AMOUNT,
|
||||||
|
BETTING_STREAK_RESET_HOUR,
|
||||||
|
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||||
|
} from '../../common/numeric-constants'
|
||||||
import {
|
import {
|
||||||
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
HOUSE_LIQUIDITY_PROVIDER_ID,
|
HOUSE_LIQUIDITY_PROVIDER_ID,
|
||||||
|
@ -38,37 +43,99 @@ export const onCreateBet = functions.firestore
|
||||||
.doc(contractId)
|
.doc(contractId)
|
||||||
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
.update({ lastBetTime, lastUpdatedTime: Date.now() })
|
||||||
|
|
||||||
await notifyFills(bet, contractId, eventId)
|
const userContractSnap = await firestore
|
||||||
await updateUniqueBettorsAndGiveCreatorBonus(
|
.collection(`contracts`)
|
||||||
contractId,
|
.doc(contractId)
|
||||||
eventId,
|
.get()
|
||||||
bet.userId
|
const contract = userContractSnap.data() as Contract
|
||||||
)
|
|
||||||
|
if (!contract) {
|
||||||
|
log(`Could not find contract ${contractId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId)
|
||||||
|
|
||||||
|
const bettor = await getUser(bet.userId)
|
||||||
|
if (!bettor) return
|
||||||
|
|
||||||
|
await notifyFills(bet, contract, eventId, bettor)
|
||||||
|
await updateBettingStreak(bettor, bet, contract, eventId)
|
||||||
|
|
||||||
|
await firestore.collection('users').doc(bettor.id).update({ lastBetTime })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateBettingStreak = async (
|
||||||
|
user: User,
|
||||||
|
bet: Bet,
|
||||||
|
contract: Contract,
|
||||||
|
eventId: string
|
||||||
|
) => {
|
||||||
|
const betStreakResetTime = getTodaysBettingStreakResetTime()
|
||||||
|
const lastBetTime = user?.lastBetTime ?? 0
|
||||||
|
|
||||||
|
// If they've already bet after the reset time, or if we haven't hit the reset time yet
|
||||||
|
if (lastBetTime > betStreakResetTime || bet.createdTime < betStreakResetTime)
|
||||||
|
return
|
||||||
|
|
||||||
|
const newBettingStreak = (user?.currentBettingStreak ?? 0) + 1
|
||||||
|
// Otherwise, add 1 to their betting streak
|
||||||
|
await firestore.collection('users').doc(user.id).update({
|
||||||
|
currentBettingStreak: newBettingStreak,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send them the bonus times their streak
|
||||||
|
const bonusAmount = Math.min(
|
||||||
|
BETTING_STREAK_BONUS_AMOUNT * newBettingStreak,
|
||||||
|
100
|
||||||
|
)
|
||||||
|
const fromUserId = isProd()
|
||||||
|
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
const bonusTxnDetails = {
|
||||||
|
currentBettingStreak: newBettingStreak,
|
||||||
|
}
|
||||||
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
|
const bonusTxn: TxnData = {
|
||||||
|
fromId: fromUserId,
|
||||||
|
fromType: 'BANK',
|
||||||
|
toId: user.id,
|
||||||
|
toType: 'USER',
|
||||||
|
amount: bonusAmount,
|
||||||
|
token: 'M$',
|
||||||
|
category: 'BETTING_STREAK_BONUS',
|
||||||
|
description: JSON.stringify(bonusTxnDetails),
|
||||||
|
}
|
||||||
|
return await runTxn(trans, bonusTxn)
|
||||||
|
})
|
||||||
|
if (!result.txn) {
|
||||||
|
log("betting streak bonus txn couldn't be made")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await createBettingStreakBonusNotification(
|
||||||
|
user,
|
||||||
|
result.txn.id,
|
||||||
|
bet,
|
||||||
|
contract,
|
||||||
|
bonusAmount,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const updateUniqueBettorsAndGiveCreatorBonus = async (
|
const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
contractId: string,
|
contract: Contract,
|
||||||
eventId: string,
|
eventId: string,
|
||||||
bettorId: string
|
bettorId: string
|
||||||
) => {
|
) => {
|
||||||
const userContractSnap = await firestore
|
|
||||||
.collection(`contracts`)
|
|
||||||
.doc(contractId)
|
|
||||||
.get()
|
|
||||||
const contract = userContractSnap.data() as Contract
|
|
||||||
if (!contract) {
|
|
||||||
log(`Could not find contract ${contractId}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let previousUniqueBettorIds = contract.uniqueBettorIds
|
let previousUniqueBettorIds = contract.uniqueBettorIds
|
||||||
|
|
||||||
if (!previousUniqueBettorIds) {
|
if (!previousUniqueBettorIds) {
|
||||||
const contractBets = (
|
const contractBets = (
|
||||||
await firestore.collection(`contracts/${contractId}/bets`).get()
|
await firestore.collection(`contracts/${contract.id}/bets`).get()
|
||||||
).docs.map((doc) => doc.data() as Bet)
|
).docs.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
if (contractBets.length === 0) {
|
if (contractBets.length === 0) {
|
||||||
log(`No bets for contract ${contractId}`)
|
log(`No bets for contract ${contract.id}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +153,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
if (!contract.uniqueBettorIds || isNewUniqueBettor) {
|
||||||
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
log(`Got ${previousUniqueBettorIds} unique bettors`)
|
||||||
isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`)
|
isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`)
|
||||||
await firestore.collection(`contracts`).doc(contractId).update({
|
await firestore.collection(`contracts`).doc(contract.id).update({
|
||||||
uniqueBettorIds: newUniqueBettorIds,
|
uniqueBettorIds: newUniqueBettorIds,
|
||||||
uniqueBettorCount: newUniqueBettorIds.length,
|
uniqueBettorCount: newUniqueBettorIds.length,
|
||||||
})
|
})
|
||||||
|
@ -97,7 +164,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
|
|
||||||
// Create combined txn for all new unique bettors
|
// Create combined txn for all new unique bettors
|
||||||
const bonusTxnDetails = {
|
const bonusTxnDetails = {
|
||||||
contractId: contractId,
|
contractId: contract.id,
|
||||||
uniqueBettorIds: newUniqueBettorIds,
|
uniqueBettorIds: newUniqueBettorIds,
|
||||||
}
|
}
|
||||||
const fromUserId = isProd()
|
const fromUserId = isProd()
|
||||||
|
@ -140,14 +207,14 @@ const updateUniqueBettorsAndGiveCreatorBonus = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
const notifyFills = async (
|
||||||
|
bet: Bet,
|
||||||
|
contract: Contract,
|
||||||
|
eventId: string,
|
||||||
|
user: User
|
||||||
|
) => {
|
||||||
if (!bet.fills) return
|
if (!bet.fills) return
|
||||||
|
|
||||||
const user = await getUser(bet.userId)
|
|
||||||
if (!user) return
|
|
||||||
const contract = await getContract(contractId)
|
|
||||||
if (!contract) return
|
|
||||||
|
|
||||||
const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null)
|
const matchedFills = bet.fills.filter((fill) => fill.matchedBetId !== null)
|
||||||
const matchedBets = (
|
const matchedBets = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -180,3 +247,7 @@ const notifyFills = async (bet: Bet, contractId: string, eventId: string) => {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTodaysBettingStreakResetTime = () => {
|
||||||
|
return new Date().setUTCHours(BETTING_STREAK_RESET_HOUR, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
38
functions/src/reset-betting-streaks.ts
Normal file
38
functions/src/reset-betting-streaks.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// check every day if the user has created a bet since 4pm UTC, and if not, reset their streak
|
||||||
|
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { User } from '../../common/user'
|
||||||
|
import { DAY_MS } from '../../common/util/time'
|
||||||
|
import { BETTING_STREAK_RESET_HOUR } from '../../common/numeric-constants'
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const resetBettingStreaksForUsers = functions.pubsub
|
||||||
|
.schedule(`0 ${BETTING_STREAK_RESET_HOUR} * * *`)
|
||||||
|
.onRun(async () => {
|
||||||
|
await resetBettingStreaksInternal()
|
||||||
|
})
|
||||||
|
|
||||||
|
const resetBettingStreaksInternal = async () => {
|
||||||
|
const usersSnap = await firestore.collection('users').get()
|
||||||
|
|
||||||
|
const users = usersSnap.docs.map((doc) => doc.data() as User)
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
await resetBettingStreakForUser(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetBettingStreakForUser = async (user: User) => {
|
||||||
|
const betStreakResetTime = Date.now() - DAY_MS
|
||||||
|
// if they made a bet within the last day, don't reset their streak
|
||||||
|
if (
|
||||||
|
(user.lastBetTime ?? 0 > betStreakResetTime) ||
|
||||||
|
!user.currentBettingStreak ||
|
||||||
|
user.currentBettingStreak === 0
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await firestore.collection('users').doc(user.id).update({
|
||||||
|
currentBettingStreak: 0,
|
||||||
|
})
|
||||||
|
}
|
32
web/components/profile/betting-streak-modal.tsx
Normal file
32
web/components/profile/betting-streak-modal.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Modal } from 'web/components/layout/modal'
|
||||||
|
import { Col } from 'web/components/layout/col'
|
||||||
|
|
||||||
|
export function BettingStreakModal(props: {
|
||||||
|
isOpen: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { isOpen, setOpen } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} setOpen={setOpen}>
|
||||||
|
<Col className="items-center gap-4 rounded-md bg-white px-8 py-6">
|
||||||
|
<span className={'text-8xl'}>🔥</span>
|
||||||
|
<span>Betting streaks are here!</span>
|
||||||
|
<Col className={'gap-2'}>
|
||||||
|
<span className={'text-indigo-700'}>• What are they?</span>
|
||||||
|
<span className={'ml-2'}>
|
||||||
|
You get a reward for every consecutive day that you place a bet. The
|
||||||
|
more days you bet in a row, the more you earn!
|
||||||
|
</span>
|
||||||
|
<span className={'text-indigo-700'}>
|
||||||
|
• Where can I check my streak?
|
||||||
|
</span>
|
||||||
|
<span className={'ml-2'}>
|
||||||
|
You can see your current streak on the top right of your profile
|
||||||
|
page.
|
||||||
|
</span>
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import { ReferralsButton } from 'web/components/referrals-button'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { ShareIconButton } from 'web/components/share-icon-button'
|
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { BettingStreakModal } from 'web/components/profile/betting-streak-modal'
|
||||||
|
|
||||||
export function UserLink(props: {
|
export function UserLink(props: {
|
||||||
name: string
|
name: string
|
||||||
|
@ -65,10 +66,13 @@ export function UserPage(props: { user: User }) {
|
||||||
const isCurrentUser = user.id === currentUser?.id
|
const isCurrentUser = user.id === currentUser?.id
|
||||||
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
|
const bannerUrl = user.bannerUrl ?? defaultBannerUrl(user.id)
|
||||||
const [showConfetti, setShowConfetti] = useState(false)
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
const [showBettingStreakModal, setShowBettingStreakModal] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const claimedMana = router.query['claimed-mana'] === 'yes'
|
const claimedMana = router.query['claimed-mana'] === 'yes'
|
||||||
setShowConfetti(claimedMana)
|
setShowConfetti(claimedMana)
|
||||||
|
const showBettingStreak = router.query['show'] === 'betting-streak'
|
||||||
|
setShowBettingStreakModal(showBettingStreak)
|
||||||
}, [router])
|
}, [router])
|
||||||
|
|
||||||
const profit = user.profitCached.allTime
|
const profit = user.profitCached.allTime
|
||||||
|
@ -80,9 +84,14 @@ export function UserPage(props: { user: User }) {
|
||||||
description={user.bio ?? ''}
|
description={user.bio ?? ''}
|
||||||
url={`/${user.username}`}
|
url={`/${user.username}`}
|
||||||
/>
|
/>
|
||||||
{showConfetti && (
|
{showConfetti ||
|
||||||
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
(showBettingStreakModal && (
|
||||||
)}
|
<FullscreenConfetti recycle={false} numberOfPieces={300} />
|
||||||
|
))}
|
||||||
|
<BettingStreakModal
|
||||||
|
isOpen={showBettingStreakModal}
|
||||||
|
setOpen={setShowBettingStreakModal}
|
||||||
|
/>
|
||||||
{/* Banner image up top, with an circle avatar overlaid */}
|
{/* Banner image up top, with an circle avatar overlaid */}
|
||||||
<div
|
<div
|
||||||
className="h-32 w-full bg-cover bg-center sm:h-40"
|
className="h-32 w-full bg-cover bg-center sm:h-40"
|
||||||
|
@ -114,22 +123,34 @@ export function UserPage(props: { user: User }) {
|
||||||
|
|
||||||
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
{/* Profile details: name, username, bio, and link to twitter/discord */}
|
||||||
<Col className="mx-4 -mt-6">
|
<Col className="mx-4 -mt-6">
|
||||||
<Row className={'items-center gap-2'}>
|
<Row className={'justify-between'}>
|
||||||
<span className="text-2xl font-bold">{user.name}</span>
|
<Col>
|
||||||
<span className="mt-1 text-gray-500">
|
<span className="text-2xl font-bold">{user.name}</span>
|
||||||
<span
|
<span className="text-gray-500">@{user.username}</span>
|
||||||
className={clsx(
|
</Col>
|
||||||
'text-md',
|
<Col className={'justify-center gap-4'}>
|
||||||
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
<Row>
|
||||||
)}
|
<Col className={'items-center text-gray-500'}>
|
||||||
>
|
<span
|
||||||
{formatMoney(profit)}
|
className={clsx(
|
||||||
</span>{' '}
|
'text-md',
|
||||||
profit
|
profit >= 0 ? 'text-green-600' : 'text-red-400'
|
||||||
</span>
|
)}
|
||||||
|
>
|
||||||
|
{formatMoney(profit)}
|
||||||
|
</span>
|
||||||
|
<span>profit</span>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
className={'cursor-pointer items-center text-gray-500'}
|
||||||
|
onClick={() => setShowBettingStreakModal(true)}
|
||||||
|
>
|
||||||
|
<span>🔥{user.currentBettingStreak ?? 0}</span>
|
||||||
|
<span>streak</span>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<span className="text-gray-500">@{user.username}</span>
|
|
||||||
|
|
||||||
<Spacer h={4} />
|
<Spacer h={4} />
|
||||||
{user.bio && (
|
{user.bio && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -71,11 +71,15 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
||||||
const incomeNotifications = notificationsGroupedByDay.filter(
|
const incomeNotifications = notificationsGroupedByDay.filter(
|
||||||
(notification) =>
|
(notification) =>
|
||||||
notification.sourceType === 'bonus' || notification.sourceType === 'tip'
|
notification.sourceType === 'bonus' ||
|
||||||
|
notification.sourceType === 'tip' ||
|
||||||
|
notification.sourceType === 'betting_streak_bonus'
|
||||||
)
|
)
|
||||||
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
||||||
(notification) =>
|
(notification) =>
|
||||||
notification.sourceType !== 'bonus' && notification.sourceType !== 'tip'
|
notification.sourceType !== 'bonus' &&
|
||||||
|
notification.sourceType !== 'tip' &&
|
||||||
|
notification.sourceType !== 'betting_streak_bonus'
|
||||||
)
|
)
|
||||||
if (incomeNotifications.length > 0) {
|
if (incomeNotifications.length > 0) {
|
||||||
notificationGroups = notificationGroups.concat({
|
notificationGroups = notificationGroups.concat({
|
||||||
|
|
|
@ -31,7 +31,10 @@ import {
|
||||||
import { TrendingUpIcon } from '@heroicons/react/outline'
|
import { TrendingUpIcon } from '@heroicons/react/outline'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
import {
|
||||||
|
BETTING_STREAK_BONUS_AMOUNT,
|
||||||
|
UNIQUE_BETTOR_BONUS_AMOUNT,
|
||||||
|
} from 'common/numeric-constants'
|
||||||
import { groupBy, sum, uniq } from 'lodash'
|
import { groupBy, sum, uniq } from 'lodash'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { Pagination } from 'web/components/pagination'
|
import { Pagination } from 'web/components/pagination'
|
||||||
|
@ -229,39 +232,39 @@ function IncomeNotificationGroupItem(props: {
|
||||||
(n) => n.sourceType
|
(n) => n.sourceType
|
||||||
)
|
)
|
||||||
for (const sourceType in groupedNotificationsBySourceType) {
|
for (const sourceType in groupedNotificationsBySourceType) {
|
||||||
// Source title splits by contracts and groups
|
// Source title splits by contracts, groups, betting streak bonus
|
||||||
const groupedNotificationsBySourceTitle = groupBy(
|
const groupedNotificationsBySourceTitle = groupBy(
|
||||||
groupedNotificationsBySourceType[sourceType],
|
groupedNotificationsBySourceType[sourceType],
|
||||||
(notification) => {
|
(notification) => {
|
||||||
return notification.sourceTitle ?? notification.sourceContractTitle
|
return notification.sourceTitle ?? notification.sourceContractTitle
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
for (const contractId in groupedNotificationsBySourceTitle) {
|
for (const sourceTitle in groupedNotificationsBySourceTitle) {
|
||||||
const notificationsForContractId =
|
const notificationsForSourceTitle =
|
||||||
groupedNotificationsBySourceTitle[contractId]
|
groupedNotificationsBySourceTitle[sourceTitle]
|
||||||
if (notificationsForContractId.length === 1) {
|
if (notificationsForSourceTitle.length === 1) {
|
||||||
newNotifications.push(notificationsForContractId[0])
|
newNotifications.push(notificationsForSourceTitle[0])
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
let sum = 0
|
let sum = 0
|
||||||
notificationsForContractId.forEach(
|
notificationsForSourceTitle.forEach(
|
||||||
(notification) =>
|
(notification) =>
|
||||||
notification.sourceText &&
|
notification.sourceText &&
|
||||||
(sum = parseInt(notification.sourceText) + sum)
|
(sum = parseInt(notification.sourceText) + sum)
|
||||||
)
|
)
|
||||||
const uniqueUsers = uniq(
|
const uniqueUsers = uniq(
|
||||||
notificationsForContractId.map((notification) => {
|
notificationsForSourceTitle.map((notification) => {
|
||||||
return notification.sourceUserUsername
|
return notification.sourceUserUsername
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const newNotification = {
|
const newNotification = {
|
||||||
...notificationsForContractId[0],
|
...notificationsForSourceTitle[0],
|
||||||
sourceText: sum.toString(),
|
sourceText: sum.toString(),
|
||||||
sourceUserUsername:
|
sourceUserUsername:
|
||||||
uniqueUsers.length > 1
|
uniqueUsers.length > 1
|
||||||
? MULTIPLE_USERS_KEY
|
? MULTIPLE_USERS_KEY
|
||||||
: notificationsForContractId[0].sourceType,
|
: notificationsForSourceTitle[0].sourceType,
|
||||||
}
|
}
|
||||||
newNotifications.push(newNotification)
|
newNotifications.push(newNotification)
|
||||||
}
|
}
|
||||||
|
@ -362,7 +365,8 @@ function IncomeNotificationItem(props: {
|
||||||
justSummary?: boolean
|
justSummary?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { notification, justSummary } = props
|
const { notification, justSummary } = props
|
||||||
const { sourceType, sourceUserName, sourceUserUsername } = notification
|
const { sourceType, sourceUserName, sourceUserUsername, sourceText } =
|
||||||
|
notification
|
||||||
const [highlighted] = useState(!notification.isSeen)
|
const [highlighted] = useState(!notification.isSeen)
|
||||||
const { width } = useWindowSize()
|
const { width } = useWindowSize()
|
||||||
const isMobile = (width && width < 768) || false
|
const isMobile = (width && width < 768) || false
|
||||||
|
@ -370,19 +374,74 @@ function IncomeNotificationItem(props: {
|
||||||
setNotificationsAsSeen([notification])
|
setNotificationsAsSeen([notification])
|
||||||
}, [notification])
|
}, [notification])
|
||||||
|
|
||||||
function getReasonForShowingIncomeNotification(simple: boolean) {
|
function reasonAndLink(simple: boolean) {
|
||||||
const { sourceText } = notification
|
const { sourceText } = notification
|
||||||
let reasonText = ''
|
let reasonText = ''
|
||||||
if (sourceType === 'bonus' && sourceText) {
|
if (sourceType === 'bonus' && sourceText) {
|
||||||
reasonText = !simple
|
reasonText = !simple
|
||||||
? `Bonus for ${
|
? `Bonus for ${
|
||||||
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||||
} unique traders`
|
} unique traders on`
|
||||||
: 'bonus on'
|
: 'bonus on'
|
||||||
} else if (sourceType === 'tip') {
|
} else if (sourceType === 'tip') {
|
||||||
reasonText = !simple ? `tipped you` : `in tips on`
|
reasonText = !simple ? `tipped you on` : `in tips on`
|
||||||
|
} else if (sourceType === 'betting_streak_bonus' && sourceText) {
|
||||||
|
reasonText = `for your ${
|
||||||
|
parseInt(sourceText) / BETTING_STREAK_BONUS_AMOUNT
|
||||||
|
}-day`
|
||||||
}
|
}
|
||||||
return reasonText
|
return (
|
||||||
|
<>
|
||||||
|
{reasonText}
|
||||||
|
{sourceType === 'betting_streak_bonus' ? (
|
||||||
|
simple ? (
|
||||||
|
<span className={'ml-1 font-bold'}>Betting Streak</span>
|
||||||
|
) : (
|
||||||
|
<SiteLink
|
||||||
|
className={'ml-1 font-bold'}
|
||||||
|
href={'/betting-streak-bonus'}
|
||||||
|
>
|
||||||
|
Betting Streak
|
||||||
|
</SiteLink>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<QuestionOrGroupLink
|
||||||
|
notification={notification}
|
||||||
|
ignoreClick={isMobile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomeNotificationLabel = () => {
|
||||||
|
return sourceText ? (
|
||||||
|
<span className="text-primary">
|
||||||
|
{'+' + formatMoney(parseInt(sourceText))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIncomeSourceUrl = () => {
|
||||||
|
const {
|
||||||
|
sourceId,
|
||||||
|
sourceContractCreatorUsername,
|
||||||
|
sourceContractSlug,
|
||||||
|
sourceSlug,
|
||||||
|
} = notification
|
||||||
|
if (sourceType === 'tip' && sourceContractSlug)
|
||||||
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}`
|
||||||
|
if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||||
|
if (sourceType === 'challenge') return `${sourceSlug}`
|
||||||
|
if (sourceType === 'betting_streak_bonus')
|
||||||
|
return `/${sourceUserUsername}/?show=betting-streak`
|
||||||
|
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||||
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||||
|
sourceId ?? '',
|
||||||
|
sourceType
|
||||||
|
)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (justSummary) {
|
if (justSummary) {
|
||||||
|
@ -392,19 +451,9 @@ function IncomeNotificationItem(props: {
|
||||||
<div className={'flex pl-1 sm:pl-0'}>
|
<div className={'flex pl-1 sm:pl-0'}>
|
||||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||||
<div className={'mr-1 text-black'}>
|
<div className={'mr-1 text-black'}>
|
||||||
<NotificationTextLabel
|
{incomeNotificationLabel()}
|
||||||
className={'line-clamp-1'}
|
|
||||||
notification={notification}
|
|
||||||
justSummary={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<span className={'flex truncate'}>
|
<span className={'flex truncate'}>{reasonAndLink(true)}</span>
|
||||||
{getReasonForShowingIncomeNotification(true)}
|
|
||||||
<QuestionOrGroupLink
|
|
||||||
notification={notification}
|
|
||||||
ignoreClick={isMobile}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -421,18 +470,16 @@ function IncomeNotificationItem(props: {
|
||||||
>
|
>
|
||||||
<div className={'relative'}>
|
<div className={'relative'}>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
href={getSourceUrl(notification) ?? ''}
|
href={getIncomeSourceUrl() ?? ''}
|
||||||
className={'absolute left-0 right-0 top-0 bottom-0 z-0'}
|
className={'absolute left-0 right-0 top-0 bottom-0 z-0'}
|
||||||
/>
|
/>
|
||||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
<div className={'line-clamp-2 flex max-w-xl shrink '}>
|
<div className={'line-clamp-2 flex max-w-xl shrink '}>
|
||||||
<div className={'inline'}>
|
<div className={'inline'}>
|
||||||
<span className={'mr-1'}>
|
<span className={'mr-1'}>{incomeNotificationLabel()}</span>
|
||||||
<NotificationTextLabel notification={notification} />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span>
|
||||||
{sourceType != 'bonus' &&
|
{sourceType === 'tip' &&
|
||||||
(sourceUserUsername === MULTIPLE_USERS_KEY ? (
|
(sourceUserUsername === MULTIPLE_USERS_KEY ? (
|
||||||
<span className={'mr-1 truncate'}>Multiple users</span>
|
<span className={'mr-1 truncate'}>Multiple users</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -443,8 +490,7 @@ function IncomeNotificationItem(props: {
|
||||||
short={true}
|
short={true}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{getReasonForShowingIncomeNotification(false)} {' on'}
|
{reasonAndLink(false)}
|
||||||
<QuestionOrGroupLink notification={notification} />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -794,9 +840,6 @@ function getSourceUrl(notification: Notification) {
|
||||||
// User referral:
|
// User referral:
|
||||||
if (sourceType === 'user' && !sourceContractSlug)
|
if (sourceType === 'user' && !sourceContractSlug)
|
||||||
return `/${sourceUserUsername}`
|
return `/${sourceUserUsername}`
|
||||||
if (sourceType === 'tip' && sourceContractSlug)
|
|
||||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}`
|
|
||||||
if (sourceType === 'tip' && sourceSlug) return `${groupPath(sourceSlug)}`
|
|
||||||
if (sourceType === 'challenge') return `${sourceSlug}`
|
if (sourceType === 'challenge') return `${sourceSlug}`
|
||||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||||
|
@ -885,12 +928,6 @@ function NotificationTextLabel(props: {
|
||||||
return (
|
return (
|
||||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||||
)
|
)
|
||||||
} else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) {
|
|
||||||
return (
|
|
||||||
<span className="text-primary">
|
|
||||||
{'+' + formatMoney(parseInt(sourceText))}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
} else if (sourceType === 'bet' && sourceText) {
|
} else if (sourceType === 'bet' && sourceText) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user