Daily trading bonuses (#618)
* first commit, WIP * Give trading bonuses & paginate notifications * Move read & update into transaction * Move request bonus logic to notifs icon
This commit is contained in:
parent
53b4a28944
commit
b26648c1ce
|
@ -34,6 +34,7 @@ export type notification_source_types =
|
||||||
| 'admin_message'
|
| 'admin_message'
|
||||||
| 'group'
|
| 'group'
|
||||||
| 'user'
|
| 'user'
|
||||||
|
| 'bonus'
|
||||||
|
|
||||||
export type notification_source_update_types =
|
export type notification_source_update_types =
|
||||||
| 'created'
|
| 'created'
|
||||||
|
@ -56,3 +57,4 @@ export type notification_reason_types =
|
||||||
| 'added_you_to_group'
|
| 'added_you_to_group'
|
||||||
| 'you_referred_user'
|
| 'you_referred_user'
|
||||||
| 'user_joined_to_bet_on_your_market'
|
| 'user_joined_to_bet_on_your_market'
|
||||||
|
| 'unique_bettors_on_your_contract'
|
||||||
|
|
|
@ -3,3 +3,4 @@ export const NUMERIC_FIXED_VAR = 0.005
|
||||||
|
|
||||||
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
|
||||||
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
export const NUMERIC_TEXT_COLOR = 'text-blue-500'
|
||||||
|
export const UNIQUE_BETTOR_BONUS_AMOUNT = 5
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
// A txn (pronounced "texan") respresents a payment between two ids on Manifold
|
||||||
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
|
||||||
type AnyTxnType = Donation | Tip | Manalink | Referral
|
type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus
|
||||||
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
|
||||||
|
|
||||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
|
@ -16,7 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
amount: number
|
amount: number
|
||||||
token: 'M$' // | 'USD' | MarketOutcome
|
token: 'M$' // | 'USD' | MarketOutcome
|
||||||
|
|
||||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||||
|
|
||||||
// Any extra data
|
// Any extra data
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
|
||||||
|
@ -52,6 +53,12 @@ type Referral = {
|
||||||
category: 'REFERRAL'
|
category: 'REFERRAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Bonus = {
|
||||||
|
fromType: 'BANK'
|
||||||
|
toType: 'USER'
|
||||||
|
category: 'UNIQUE_BETTOR_BONUS'
|
||||||
|
}
|
||||||
|
|
||||||
export type DonationTxn = Txn & Donation
|
export type DonationTxn = Txn & Donation
|
||||||
export type TipTxn = Txn & Tip
|
export type TipTxn = Txn & Tip
|
||||||
export type ManalinkTxn = Txn & Manalink
|
export type ManalinkTxn = Txn & Manalink
|
||||||
|
|
|
@ -57,6 +57,7 @@ export type PrivateUser = {
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
notificationPreferences?: notification_subscribe_types
|
notificationPreferences?: notification_subscribe_types
|
||||||
|
lastTimeCheckedBonuses?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||||
|
|
|
@ -267,6 +267,15 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyContractCreatorOfUniqueBettorsBonus = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'unique_bettors_on_your_contract',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getUsersToNotify = async () => {
|
const getUsersToNotify = async () => {
|
||||||
const userToReasonTexts: user_to_reason_texts = {}
|
const userToReasonTexts: user_to_reason_texts = {}
|
||||||
// The following functions modify the userToReasonTexts object in place.
|
// The following functions modify the userToReasonTexts object in place.
|
||||||
|
@ -309,6 +318,12 @@ export const createNotification = async (
|
||||||
})
|
})
|
||||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||||
|
} else if (sourceType === 'bonus' && sourceUpdateType === 'created') {
|
||||||
|
// Note: the daily bonus won't have a contract attached to it
|
||||||
|
await notifyContractCreatorOfUniqueBettorsBonus(
|
||||||
|
userToReasonTexts,
|
||||||
|
sourceContract.creatorId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return userToReasonTexts
|
return userToReasonTexts
|
||||||
}
|
}
|
||||||
|
|
139
functions/src/get-daily-bonuses.ts
Normal file
139
functions/src/get-daily-bonuses.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import { APIError, newEndpoint } from './api'
|
||||||
|
import { log } from './utils'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { PrivateUser } from '../../common/lib/user'
|
||||||
|
import { uniq } from 'lodash'
|
||||||
|
import { Bet } from '../../common/lib/bet'
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||||
|
import { runTxn, TxnData } from './transact'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
import { User } from '../../common/lib/user'
|
||||||
|
import { Contract } from '../../common/lib/contract'
|
||||||
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
||||||
|
|
||||||
|
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
|
||||||
|
const QUERY_LIMIT_SECONDS = 60
|
||||||
|
|
||||||
|
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
|
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
|
||||||
|
async (trans) => {
|
||||||
|
const userSnap = await trans.get(
|
||||||
|
firestore.doc(`private-users/${auth.uid}`)
|
||||||
|
)
|
||||||
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
|
const user = userSnap.data() as PrivateUser
|
||||||
|
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
|
||||||
|
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
|
||||||
|
throw new APIError(
|
||||||
|
400,
|
||||||
|
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
|
||||||
|
)
|
||||||
|
await trans.update(userSnap.ref, {
|
||||||
|
lastTimeCheckedBonuses: Date.now(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
lastTimeCheckedBonuses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// TODO: switch to prod id
|
||||||
|
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
|
||||||
|
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
|
||||||
|
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||||
|
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||||
|
const fromUser = fromSnap.data() as User
|
||||||
|
// Get all users contracts made since implementation time
|
||||||
|
const userContractsSnap = await firestore
|
||||||
|
.collection(`contracts`)
|
||||||
|
.where('creatorId', '==', user.id)
|
||||||
|
.where('createdTime', '>=', BONUS_START_DATE)
|
||||||
|
.get()
|
||||||
|
const userContracts = userContractsSnap.docs.map(
|
||||||
|
(doc) => doc.data() as Contract
|
||||||
|
)
|
||||||
|
const nullReturn = { status: 'no bets', txn: null }
|
||||||
|
for (const contract of userContracts) {
|
||||||
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
|
const contractId = contract.id
|
||||||
|
// Get all bets made on user's contracts
|
||||||
|
const bets = (
|
||||||
|
await firestore
|
||||||
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
.where('userId', '!=', user.id)
|
||||||
|
.get()
|
||||||
|
).docs.map((bet) => bet.ref)
|
||||||
|
if (bets.length === 0) {
|
||||||
|
return nullReturn
|
||||||
|
}
|
||||||
|
const contractBetsSnap = await trans.getAll(...bets)
|
||||||
|
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
||||||
|
|
||||||
|
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
||||||
|
contractBets
|
||||||
|
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
|
||||||
|
.map((bet) => bet.userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter users for ONLY those that have made bets since the last daily bonus received time
|
||||||
|
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
|
||||||
|
contractBets
|
||||||
|
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
|
||||||
|
.map((bet) => bet.userId)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filter for users only present in the above list
|
||||||
|
const newUniqueBettorIds =
|
||||||
|
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
||||||
|
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
||||||
|
)
|
||||||
|
newUniqueBettorIds.length > 0 &&
|
||||||
|
log(
|
||||||
|
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
||||||
|
)
|
||||||
|
if (newUniqueBettorIds.length === 0) {
|
||||||
|
return nullReturn
|
||||||
|
}
|
||||||
|
// Create combined txn for all unique bettors
|
||||||
|
const bonusTxnDetails = {
|
||||||
|
contractId: contractId,
|
||||||
|
uniqueBettors: newUniqueBettorIds.length,
|
||||||
|
}
|
||||||
|
const bonusTxn: TxnData = {
|
||||||
|
fromId: fromUser.id,
|
||||||
|
fromType: 'BANK',
|
||||||
|
toId: user.id,
|
||||||
|
toType: 'USER',
|
||||||
|
amount: UNIQUE_BETTOR_BONUS_AMOUNT * newUniqueBettorIds.length,
|
||||||
|
token: 'M$',
|
||||||
|
category: 'UNIQUE_BETTOR_BONUS',
|
||||||
|
description: JSON.stringify(bonusTxnDetails),
|
||||||
|
}
|
||||||
|
return await runTxn(trans, bonusTxn)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.status != 'success' || !result.txn) {
|
||||||
|
result.status != nullReturn.status &&
|
||||||
|
log(`No bonus for user: ${user.id} - reason:`, result.status)
|
||||||
|
} else {
|
||||||
|
log(`Bonus txn for user: ${user.id} completed:`, result.txn?.id)
|
||||||
|
await createNotification(
|
||||||
|
result.txn.id,
|
||||||
|
'bonus',
|
||||||
|
'created',
|
||||||
|
fromUser,
|
||||||
|
result.txn.id,
|
||||||
|
result.txn.amount + '',
|
||||||
|
contract,
|
||||||
|
undefined,
|
||||||
|
// No need to set the user id, we'll use the contract creator id
|
||||||
|
undefined,
|
||||||
|
contract.slug,
|
||||||
|
contract.question
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { userId: user.id, message: 'success' }
|
||||||
|
})
|
|
@ -38,3 +38,4 @@ export * from './create-contract'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
export * from './create-group'
|
export * from './create-group'
|
||||||
export * from './resolve-market'
|
export * from './resolve-market'
|
||||||
|
export * from './get-daily-bonuses'
|
||||||
|
|
|
@ -182,7 +182,6 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
const { className } = props
|
const { className } = props
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const currentPage = router.pathname
|
const currentPage = router.pathname
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const navigationOptions = !user ? signedOutNavigation : getNavigation()
|
const navigationOptions = !user ? signedOutNavigation : getNavigation()
|
||||||
const mobileNavigationOptions = !user
|
const mobileNavigationOptions = !user
|
||||||
|
|
|
@ -2,17 +2,29 @@ import { BellIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { usePrivateUser, useUser } from 'web/hooks/use-user'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications'
|
import { usePreferredGroupedNotifications } from 'web/hooks/use-notifications'
|
||||||
|
import { NOTIFICATIONS_PER_PAGE } from 'web/pages/notifications'
|
||||||
|
import { requestBonuses } from 'web/lib/firebase/api-call'
|
||||||
|
|
||||||
export default function NotificationsIcon(props: { className?: string }) {
|
export default function NotificationsIcon(props: { className?: string }) {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const notifications = usePreferredGroupedNotifications(user?.id, {
|
const privateUser = usePrivateUser(user?.id)
|
||||||
|
const notifications = usePreferredGroupedNotifications(privateUser?.id, {
|
||||||
unseenOnly: true,
|
unseenOnly: true,
|
||||||
})
|
})
|
||||||
const [seen, setSeen] = useState(false)
|
const [seen, setSeen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!privateUser) return
|
||||||
|
|
||||||
|
if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 60 * 1000)
|
||||||
|
requestBonuses({}).catch((error) => {
|
||||||
|
console.log("couldn't get bonuses:", error.message)
|
||||||
|
})
|
||||||
|
}, [privateUser])
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router.pathname.endsWith('notifications')) return setSeen(true)
|
if (router.pathname.endsWith('notifications')) return setSeen(true)
|
||||||
|
@ -24,7 +36,9 @@ export default function NotificationsIcon(props: { className?: string }) {
|
||||||
<div className={'relative'}>
|
<div className={'relative'}>
|
||||||
{!seen && notifications && notifications.length > 0 && (
|
{!seen && notifications && notifications.length > 0 && (
|
||||||
<div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2">
|
<div className="-mt-0.75 absolute ml-3.5 min-w-[15px] rounded-full bg-indigo-500 p-[2px] text-center text-[10px] leading-3 text-white lg:-mt-1 lg:ml-2">
|
||||||
{notifications.length}
|
{notifications.length > NOTIFICATIONS_PER_PAGE
|
||||||
|
? `${NOTIFICATIONS_PER_PAGE}+`
|
||||||
|
: notifications.length}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<BellIcon className={clsx(props.className)} />
|
<BellIcon className={clsx(props.className)} />
|
||||||
|
|
|
@ -7,9 +7,10 @@ import { groupBy, map } from 'lodash'
|
||||||
|
|
||||||
export type NotificationGroup = {
|
export type NotificationGroup = {
|
||||||
notifications: Notification[]
|
notifications: Notification[]
|
||||||
sourceContractId: string
|
groupedById: string
|
||||||
isSeen: boolean
|
isSeen: boolean
|
||||||
timePeriod: string
|
timePeriod: string
|
||||||
|
type: 'income' | 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePreferredGroupedNotifications(
|
export function usePreferredGroupedNotifications(
|
||||||
|
@ -37,25 +38,43 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
new Date(notification.createdTime).toDateString()
|
new Date(notification.createdTime).toDateString()
|
||||||
)
|
)
|
||||||
Object.keys(notificationGroupsByDay).forEach((day) => {
|
Object.keys(notificationGroupsByDay).forEach((day) => {
|
||||||
// Group notifications by contract:
|
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
||||||
|
const bonusNotifications = notificationsGroupedByDay.filter(
|
||||||
|
(notification) => notification.sourceType === 'bonus'
|
||||||
|
)
|
||||||
|
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
||||||
|
(notification) => notification.sourceType !== 'bonus'
|
||||||
|
)
|
||||||
|
if (bonusNotifications.length > 0) {
|
||||||
|
notificationGroups = notificationGroups.concat({
|
||||||
|
notifications: bonusNotifications,
|
||||||
|
groupedById: 'income' + day,
|
||||||
|
isSeen: bonusNotifications[0].isSeen,
|
||||||
|
timePeriod: day,
|
||||||
|
type: 'income',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Group notifications by contract, filtering out bonuses:
|
||||||
const groupedNotificationsByContractId = groupBy(
|
const groupedNotificationsByContractId = groupBy(
|
||||||
notificationGroupsByDay[day],
|
normalNotificationsGroupedByDay,
|
||||||
(notification) => {
|
(notification) => {
|
||||||
return notification.sourceContractId
|
return notification.sourceContractId
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
notificationGroups = notificationGroups.concat(
|
notificationGroups = notificationGroups.concat(
|
||||||
map(groupedNotificationsByContractId, (notifications, contractId) => {
|
map(groupedNotificationsByContractId, (notifications, contractId) => {
|
||||||
|
const notificationsForContractId = groupedNotificationsByContractId[
|
||||||
|
contractId
|
||||||
|
].sort((a, b) => {
|
||||||
|
return b.createdTime - a.createdTime
|
||||||
|
})
|
||||||
// Create a notification group for each contract within each day
|
// Create a notification group for each contract within each day
|
||||||
const notificationGroup: NotificationGroup = {
|
const notificationGroup: NotificationGroup = {
|
||||||
notifications: groupedNotificationsByContractId[contractId].sort(
|
notifications: notificationsForContractId,
|
||||||
(a, b) => {
|
groupedById: contractId,
|
||||||
return b.createdTime - a.createdTime
|
isSeen: notificationsForContractId[0].isSeen,
|
||||||
}
|
|
||||||
),
|
|
||||||
sourceContractId: contractId,
|
|
||||||
isSeen: groupedNotificationsByContractId[contractId][0].isSeen,
|
|
||||||
timePeriod: day,
|
timePeriod: day,
|
||||||
|
type: 'normal',
|
||||||
}
|
}
|
||||||
return notificationGroup
|
return notificationGroup
|
||||||
})
|
})
|
||||||
|
|
|
@ -73,3 +73,7 @@ export function sellBet(params: any) {
|
||||||
export function createGroup(params: any) {
|
export function createGroup(params: any) {
|
||||||
return call(getFunctionUrl('creategroup'), 'POST', params)
|
return call(getFunctionUrl('creategroup'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function requestBonuses(params: any) {
|
||||||
|
return call(getFunctionUrl('getdailybonuses'), 'POST', params)
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import {
|
import { Notification } from 'common/notification'
|
||||||
Notification,
|
|
||||||
notification_reason_types,
|
|
||||||
notification_source_types,
|
|
||||||
notification_source_update_types,
|
|
||||||
} from 'common/notification'
|
|
||||||
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
|
@ -31,47 +26,40 @@ import {
|
||||||
ProbPercentLabel,
|
ProbPercentLabel,
|
||||||
} from 'web/components/outcome-label'
|
} from 'web/components/outcome-label'
|
||||||
import {
|
import {
|
||||||
groupNotifications,
|
|
||||||
NotificationGroup,
|
NotificationGroup,
|
||||||
usePreferredGroupedNotifications,
|
usePreferredGroupedNotifications,
|
||||||
} from 'web/hooks/use-notifications'
|
} from 'web/hooks/use-notifications'
|
||||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
import { CheckIcon, TrendingUpIcon, XIcon } from '@heroicons/react/outline'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { groupPath } from 'web/lib/firebase/groups'
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
||||||
|
import { groupBy } from 'lodash'
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_PER_PAGE = 30
|
||||||
|
export const HIGHLIGHT_DURATION = 30 * 1000
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [unseenNotificationGroups, setUnseenNotificationGroups] = useState<
|
const [page, setPage] = useState(1)
|
||||||
NotificationGroup[] | undefined
|
|
||||||
>(undefined)
|
const groupedNotifications = usePreferredGroupedNotifications(user?.id, {
|
||||||
const allNotificationGroups = usePreferredGroupedNotifications(user?.id, {
|
|
||||||
unseenOnly: false,
|
unseenOnly: false,
|
||||||
})
|
})
|
||||||
|
const [paginatedNotificationGroups, setPaginatedNotificationGroups] =
|
||||||
|
useState<NotificationGroup[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!allNotificationGroups) return
|
if (!groupedNotifications) return
|
||||||
// Don't re-add notifications that are visible right now or have been seen already.
|
const start = (page - 1) * NOTIFICATIONS_PER_PAGE
|
||||||
const currentlyVisibleUnseenNotificationIds = Object.values(
|
const end = start + NOTIFICATIONS_PER_PAGE
|
||||||
unseenNotificationGroups ?? []
|
const maxNotificationsToShow = groupedNotifications.slice(start, end)
|
||||||
)
|
const remainingNotification = groupedNotifications.slice(end)
|
||||||
.map((n) => n.notifications.map((n) => n.id))
|
for (const notification of remainingNotification) {
|
||||||
.flat()
|
if (notification.isSeen) break
|
||||||
const unseenGroupedNotifications = groupNotifications(
|
else setNotificationsAsSeen(notification.notifications)
|
||||||
allNotificationGroups
|
}
|
||||||
.map((notification: NotificationGroup) => notification.notifications)
|
setPaginatedNotificationGroups(maxNotificationsToShow)
|
||||||
.flat()
|
}, [groupedNotifications, page])
|
||||||
.filter(
|
|
||||||
(notification: Notification) =>
|
|
||||||
!notification.isSeen ||
|
|
||||||
currentlyVisibleUnseenNotificationIds.includes(notification.id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
setUnseenNotificationGroups(unseenGroupedNotifications)
|
|
||||||
|
|
||||||
// We don't want unseenNotificationsGroup to be in the dependencies as we update it here.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [allNotificationGroups])
|
|
||||||
|
|
||||||
if (user === undefined) {
|
if (user === undefined) {
|
||||||
return <LoadingIndicator />
|
return <LoadingIndicator />
|
||||||
|
@ -80,7 +68,6 @@ export default function Notifications() {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use infinite scroll
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className={'p-2 sm:p-4'}>
|
<div className={'p-2 sm:p-4'}>
|
||||||
|
@ -90,53 +77,74 @@ export default function Notifications() {
|
||||||
defaultIndex={0}
|
defaultIndex={0}
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
title: 'New Notifications',
|
title: 'Notifications',
|
||||||
content: unseenNotificationGroups ? (
|
content: groupedNotifications ? (
|
||||||
<div className={''}>
|
<div className={''}>
|
||||||
{unseenNotificationGroups.length === 0 &&
|
{paginatedNotificationGroups.length === 0 &&
|
||||||
"You don't have any new notifications."}
|
"You don't have any notifications. Try changing your settings to see more."}
|
||||||
{unseenNotificationGroups.map((notification) =>
|
{paginatedNotificationGroups.map((notification) =>
|
||||||
notification.notifications.length === 1 ? (
|
notification.notifications.length === 1 ? (
|
||||||
<NotificationItem
|
<NotificationItem
|
||||||
notification={notification.notifications[0]}
|
notification={notification.notifications[0]}
|
||||||
key={notification.notifications[0].id}
|
key={notification.notifications[0].id}
|
||||||
/>
|
/>
|
||||||
|
) : notification.type === 'income' ? (
|
||||||
|
<IncomeNotificationGroupItem
|
||||||
|
notificationGroup={notification}
|
||||||
|
key={notification.groupedById + notification.timePeriod}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<NotificationGroupItem
|
<NotificationGroupItem
|
||||||
notificationGroup={notification}
|
notificationGroup={notification}
|
||||||
key={
|
key={notification.groupedById + notification.timePeriod}
|
||||||
notification.sourceContractId +
|
|
||||||
notification.timePeriod
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
{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>
|
||||||
) : (
|
<div className="flex flex-1 justify-between sm:justify-end">
|
||||||
<LoadingIndicator />
|
<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)}
|
||||||
title: 'All Notifications',
|
>
|
||||||
content: allNotificationGroups ? (
|
Previous
|
||||||
<div className={''}>
|
</a>
|
||||||
{allNotificationGroups.length === 0 &&
|
<a
|
||||||
"You don't have any notifications. Try changing your settings to see more."}
|
href="#"
|
||||||
{allNotificationGroups.map((notification) =>
|
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"
|
||||||
notification.notifications.length === 1 ? (
|
onClick={() =>
|
||||||
<NotificationItem
|
page <
|
||||||
notification={notification.notifications[0]}
|
groupedNotifications?.length /
|
||||||
key={notification.notifications[0].id}
|
NOTIFICATIONS_PER_PAGE && setPage(page + 1)
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<NotificationGroupItem
|
|
||||||
notificationGroup={notification}
|
|
||||||
key={
|
|
||||||
notification.sourceContractId +
|
|
||||||
notification.timePeriod
|
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
)
|
Next
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -164,7 +172,6 @@ const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||||
updateDoc(
|
updateDoc(
|
||||||
doc(db, `users/${notification.userId}/notifications/`, notification.id),
|
doc(db, `users/${notification.userId}/notifications/`, notification.id),
|
||||||
{
|
{
|
||||||
...notification,
|
|
||||||
isSeen: true,
|
isSeen: true,
|
||||||
viewTime: new Date(),
|
viewTime: new Date(),
|
||||||
}
|
}
|
||||||
|
@ -173,6 +180,152 @@ const setNotificationsAsSeen = (notifications: Notification[]) => {
|
||||||
return notifications
|
return notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IncomeNotificationGroupItem(props: {
|
||||||
|
notificationGroup: NotificationGroup
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { notificationGroup, className } = props
|
||||||
|
const { notifications } = notificationGroup
|
||||||
|
const numSummaryLines = 3
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (notifications.some((n) => !n.isSeen)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
|
setNotificationsAsSeen(notifications)
|
||||||
|
}, [notifications])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded) setHighlighted(false)
|
||||||
|
}, [expanded])
|
||||||
|
|
||||||
|
const totalIncome = notifications.reduce(
|
||||||
|
(acc, notification) =>
|
||||||
|
acc +
|
||||||
|
(notification.sourceType &&
|
||||||
|
notification.sourceText &&
|
||||||
|
notification.sourceType === 'bonus'
|
||||||
|
? parseInt(notification.sourceText)
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
// loop through the contracts and combine the notification items into one
|
||||||
|
function combineNotificationsByAddingSourceTextsAndReturningTheRest(
|
||||||
|
notifications: Notification[]
|
||||||
|
) {
|
||||||
|
const newNotifications = []
|
||||||
|
const groupedNotificationsByContractId = groupBy(
|
||||||
|
notifications,
|
||||||
|
(notification) => {
|
||||||
|
return notification.sourceContractId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for (const contractId in groupedNotificationsByContractId) {
|
||||||
|
const notificationsForContractId =
|
||||||
|
groupedNotificationsByContractId[contractId]
|
||||||
|
let sum = 0
|
||||||
|
notificationsForContractId.forEach(
|
||||||
|
(notification) =>
|
||||||
|
notification.sourceText &&
|
||||||
|
(sum = parseInt(notification.sourceText) + sum)
|
||||||
|
)
|
||||||
|
|
||||||
|
const newNotification =
|
||||||
|
notificationsForContractId.length === 1
|
||||||
|
? notificationsForContractId[0]
|
||||||
|
: {
|
||||||
|
...notificationsForContractId[0],
|
||||||
|
sourceText: sum.toString(),
|
||||||
|
}
|
||||||
|
newNotifications.push(newNotification)
|
||||||
|
}
|
||||||
|
return newNotifications
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedNotifs =
|
||||||
|
combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
||||||
|
className,
|
||||||
|
!expanded ? 'hover:bg-gray-100' : '',
|
||||||
|
highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : ''
|
||||||
|
)}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
{expanded && (
|
||||||
|
<span
|
||||||
|
className="absolute top-14 left-6 -ml-px h-[calc(100%-5rem)] w-0.5 bg-gray-200"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
|
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||||
|
<div className={'flex-1 overflow-hidden pl-2 sm:flex'}>
|
||||||
|
<div
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{'Daily Income Summary: '}
|
||||||
|
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<RelativeTimestamp time={notifications[0].createdTime} />
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}>
|
||||||
|
{' '}
|
||||||
|
<div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}>
|
||||||
|
{!expanded ? (
|
||||||
|
<>
|
||||||
|
{combinedNotifs
|
||||||
|
.slice(0, numSummaryLines)
|
||||||
|
.map((notification) => {
|
||||||
|
return (
|
||||||
|
<NotificationItem
|
||||||
|
notification={notification}
|
||||||
|
justSummary={true}
|
||||||
|
key={notification.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div className={'text-sm text-gray-500 hover:underline '}>
|
||||||
|
{combinedNotifs.length - numSummaryLines > 0
|
||||||
|
? 'And ' +
|
||||||
|
(combinedNotifs.length - numSummaryLines) +
|
||||||
|
' more...'
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{combinedNotifs.map((notification) => (
|
||||||
|
<NotificationItem
|
||||||
|
notification={notification}
|
||||||
|
key={notification.id}
|
||||||
|
justSummary={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'mt-6 border-b border-gray-300'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function NotificationGroupItem(props: {
|
function NotificationGroupItem(props: {
|
||||||
notificationGroup: NotificationGroup
|
notificationGroup: NotificationGroup
|
||||||
className?: string
|
className?: string
|
||||||
|
@ -187,17 +340,28 @@ function NotificationGroupItem(props: {
|
||||||
const numSummaryLines = 3
|
const numSummaryLines = 3
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (notifications.some((n) => !n.isSeen)) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
setNotificationsAsSeen(notifications)
|
setNotificationsAsSeen(notifications)
|
||||||
}, [notifications])
|
}, [notifications])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (expanded) setHighlighted(false)
|
||||||
|
}, [expanded])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
||||||
className,
|
className,
|
||||||
!expanded ? 'hover:bg-gray-100' : ''
|
!expanded ? 'hover:bg-gray-100' : '',
|
||||||
|
highlighted && !expanded ? 'bg-indigo-200 hover:bg-indigo-100' : ''
|
||||||
)}
|
)}
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
>
|
>
|
||||||
|
@ -432,7 +596,7 @@ function NotificationSettings() {
|
||||||
/>
|
/>
|
||||||
<NotificationSettingLine
|
<NotificationSettingLine
|
||||||
highlight={notificationSettings !== 'none'}
|
highlight={notificationSettings !== 'none'}
|
||||||
label={"Referral bonuses you've received"}
|
label={"Income & referral bonuses you've received"}
|
||||||
/>
|
/>
|
||||||
<NotificationSettingLine
|
<NotificationSettingLine
|
||||||
label={"Activity on questions you've ever bet or commented on"}
|
label={"Activity on questions you've ever bet or commented on"}
|
||||||
|
@ -476,17 +640,6 @@ function NotificationSettings() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotificationAboutContractResolution(
|
|
||||||
sourceType: notification_source_types | undefined,
|
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
|
||||||
contract: Contract | null | undefined
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
(sourceType === 'contract' && sourceUpdateType === 'resolved') ||
|
|
||||||
(sourceType === 'contract' && !sourceUpdateType && contract?.resolution)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationItem(props: {
|
function NotificationItem(props: {
|
||||||
notification: Notification
|
notification: Notification
|
||||||
justSummary?: boolean
|
justSummary?: boolean
|
||||||
|
@ -522,6 +675,16 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
}, [reasonText, sourceText])
|
}, [reasonText, sourceText])
|
||||||
|
|
||||||
|
const [highlighted, setHighlighted] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!notification.isSeen) {
|
||||||
|
setHighlighted(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setHighlighted(false)
|
||||||
|
}, HIGHLIGHT_DURATION)
|
||||||
|
}
|
||||||
|
}, [notification.isSeen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotificationsAsSeen([notification])
|
setNotificationsAsSeen([notification])
|
||||||
}, [notification])
|
}, [notification])
|
||||||
|
@ -559,22 +722,21 @@ function NotificationItem(props: {
|
||||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||||
<div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}>
|
<div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}>
|
||||||
<div className={'flex pl-1 sm:pl-0'}>
|
<div className={'flex pl-1 sm:pl-0'}>
|
||||||
|
{sourceType != 'bonus' && (
|
||||||
<UserLink
|
<UserLink
|
||||||
name={sourceUserName || ''}
|
name={sourceUserName || ''}
|
||||||
username={sourceUserUsername || ''}
|
username={sourceUserUsername || ''}
|
||||||
className={'mr-0 flex-shrink-0'}
|
className={'mr-0 flex-shrink-0'}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||||
<span className={'flex-shrink-0'}>
|
<span className={'flex-shrink-0'}>
|
||||||
{sourceType &&
|
{sourceType &&
|
||||||
reason &&
|
reason &&
|
||||||
getReasonForShowingNotification(
|
getReasonForShowingNotification(notification, true).replace(
|
||||||
sourceType,
|
' on',
|
||||||
reason,
|
''
|
||||||
sourceUpdateType,
|
)}
|
||||||
undefined,
|
|
||||||
true
|
|
||||||
).replace(' on', '')}
|
|
||||||
</span>
|
</span>
|
||||||
<div className={'ml-1 text-black'}>
|
<div className={'ml-1 text-black'}>
|
||||||
<NotificationTextLabel
|
<NotificationTextLabel
|
||||||
|
@ -593,37 +755,41 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'bg-white px-2 pt-6 text-sm sm:px-4'}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'bg-white px-2 pt-6 text-sm sm:px-4',
|
||||||
|
highlighted && 'bg-indigo-200'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<a href={getSourceUrl()}>
|
<a href={getSourceUrl()}>
|
||||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||||
|
{sourceType != 'bonus' ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={sourceUserAvatarUrl}
|
avatarUrl={sourceUserAvatarUrl}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
className={'mr-2'}
|
className={'mr-2'}
|
||||||
username={sourceUserName}
|
username={sourceUserName}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||||
|
)}
|
||||||
<div className={'flex-1 overflow-hidden sm:flex'}>
|
<div className={'flex-1 overflow-hidden sm:flex'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{sourceType != 'bonus' && sourceUpdateType != 'closed' && (
|
||||||
<UserLink
|
<UserLink
|
||||||
name={sourceUserName || ''}
|
name={sourceUserName || ''}
|
||||||
username={sourceUserUsername || ''}
|
username={sourceUserUsername || ''}
|
||||||
className={'mr-0 flex-shrink-0'}
|
className={'mr-0 flex-shrink-0'}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||||
{sourceType && reason && (
|
{sourceType && reason && (
|
||||||
<div className={'inline truncate'}>
|
<div className={'inline truncate'}>
|
||||||
{getReasonForShowingNotification(
|
{getReasonForShowingNotification(notification, false)}
|
||||||
sourceType,
|
|
||||||
reason,
|
|
||||||
sourceUpdateType,
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
sourceSlug
|
|
||||||
)}
|
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
sourceContractCreatorUsername
|
sourceContractCreatorUsername
|
||||||
|
@ -684,13 +850,7 @@ function NotificationTextLabel(props: {
|
||||||
return <span>{contract?.question || sourceContractTitle}</span>
|
return <span>{contract?.question || sourceContractTitle}</span>
|
||||||
if (!sourceText) return <div />
|
if (!sourceText) return <div />
|
||||||
// Resolved contracts
|
// Resolved contracts
|
||||||
if (
|
if (sourceType === 'contract' && sourceUpdateType === 'resolved') {
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
{
|
{
|
||||||
if (sourceText === 'YES' || sourceText == 'NO') {
|
if (sourceText === 'YES' || sourceText == 'NO') {
|
||||||
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
||||||
|
@ -730,6 +890,12 @@ function NotificationTextLabel(props: {
|
||||||
return (
|
return (
|
||||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||||
)
|
)
|
||||||
|
} else if (sourceType === 'bonus' && sourceText) {
|
||||||
|
return (
|
||||||
|
<span className="text-primary">
|
||||||
|
{'+' + formatMoney(parseInt(sourceText))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// return default text
|
// return default text
|
||||||
return (
|
return (
|
||||||
|
@ -740,15 +906,13 @@ function NotificationTextLabel(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReasonForShowingNotification(
|
function getReasonForShowingNotification(
|
||||||
source: notification_source_types,
|
notification: Notification,
|
||||||
reason: notification_reason_types,
|
simple?: boolean
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
|
||||||
contract: Contract | undefined | null,
|
|
||||||
simple?: boolean,
|
|
||||||
sourceSlug?: string
|
|
||||||
) {
|
) {
|
||||||
|
const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } =
|
||||||
|
notification
|
||||||
let reasonText: string
|
let reasonText: string
|
||||||
switch (source) {
|
switch (sourceType) {
|
||||||
case 'comment':
|
case 'comment':
|
||||||
if (reason === 'reply_to_users_answer')
|
if (reason === 'reply_to_users_answer')
|
||||||
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
||||||
|
@ -768,16 +932,9 @@ function getReasonForShowingNotification(
|
||||||
break
|
break
|
||||||
case 'contract':
|
case 'contract':
|
||||||
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
||||||
else if (
|
else if (sourceUpdateType === 'resolved') reasonText = `resolved`
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
source,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
)
|
|
||||||
reasonText = `resolved`
|
|
||||||
else if (sourceUpdateType === 'closed')
|
else if (sourceUpdateType === 'closed')
|
||||||
reasonText = `please resolve your question`
|
reasonText = `Please resolve your question`
|
||||||
else reasonText = `updated`
|
else reasonText = `updated`
|
||||||
break
|
break
|
||||||
case 'answer':
|
case 'answer':
|
||||||
|
@ -805,6 +962,15 @@ function getReasonForShowingNotification(
|
||||||
else if (sourceSlug) reasonText = 'joined because you shared'
|
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||||
else reasonText = 'joined because of you'
|
else reasonText = 'joined because of you'
|
||||||
break
|
break
|
||||||
|
case 'bonus':
|
||||||
|
if (reason === 'unique_bettors_on_your_contract' && sourceText)
|
||||||
|
reasonText = !simple
|
||||||
|
? `You had ${
|
||||||
|
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
|
||||||
|
} unique bettors on`
|
||||||
|
: 'You earned Mana for unique bettors:'
|
||||||
|
else reasonText = 'You earned your daily manna'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
reasonText = ''
|
reasonText = ''
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user