Referrals (#592)
* add trigger for updated user * Add referral bonuses and notifications for them * Cleanup * Add share group button, cleanup * Cleanup * Add referrals list to user profile * Remove unused * Referral bonus => constant * Refactor * Add referral txn to helper fn * Move reads into firebase transaction * Use effects to write referral info * Flex-wrap profile objects * Small ui changes * Restrict referral user to one update * Remove rogue semicolon * Note about group referral query details * Track referrals, add them to settings list
This commit is contained in:
parent
b0b8c6e98b
commit
3165e42119
|
@ -33,6 +33,7 @@ export type notification_source_types =
|
||||||
| 'tip'
|
| 'tip'
|
||||||
| 'admin_message'
|
| 'admin_message'
|
||||||
| 'group'
|
| 'group'
|
||||||
|
| 'user'
|
||||||
|
|
||||||
export type notification_source_update_types =
|
export type notification_source_update_types =
|
||||||
| 'created'
|
| 'created'
|
||||||
|
@ -53,3 +54,5 @@ export type notification_reason_types =
|
||||||
| 'on_new_follow'
|
| 'on_new_follow'
|
||||||
| 'you_follow_user'
|
| 'you_follow_user'
|
||||||
| 'added_you_to_group'
|
| 'added_you_to_group'
|
||||||
|
| 'you_referred_user'
|
||||||
|
| 'user_joined_to_bet_on_your_market'
|
||||||
|
|
|
@ -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
|
type AnyTxnType = Donation | Tip | Manalink | Referral
|
||||||
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,7 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
amount: number
|
amount: number
|
||||||
token: 'M$' // | 'USD' | MarketOutcome
|
token: 'M$' // | 'USD' | MarketOutcome
|
||||||
|
|
||||||
category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET'
|
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
||||||
// Any extra data
|
// Any extra data
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
|
||||||
|
@ -46,6 +46,13 @@ type Manalink = {
|
||||||
category: 'MANALINK'
|
category: 'MANALINK'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Referral = {
|
||||||
|
fromType: 'BANK'
|
||||||
|
toType: 'USER'
|
||||||
|
category: 'REFERRAL'
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
export type ReferralTxn = Txn & Referral
|
||||||
|
|
|
@ -33,11 +33,14 @@ export type User = {
|
||||||
followerCountCached: number
|
followerCountCached: number
|
||||||
|
|
||||||
followedCategories?: string[]
|
followedCategories?: string[]
|
||||||
|
|
||||||
|
referredByUserId?: string
|
||||||
|
referredByContractId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STARTING_BALANCE = 1000
|
export const STARTING_BALANCE = 1000
|
||||||
export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
|
export const SUS_STARTING_BALANCE = 10 // for sus users, i.e. multiple sign ups for same person
|
||||||
|
export const REFERRAL_AMOUNT = 500
|
||||||
export type PrivateUser = {
|
export type PrivateUser = {
|
||||||
id: string // same as User.id
|
id: string // same as User.id
|
||||||
username: string // denormalized from User
|
username: string // denormalized from User
|
||||||
|
|
|
@ -20,7 +20,12 @@ service cloud.firestore {
|
||||||
allow read;
|
allow read;
|
||||||
allow update: if resource.data.id == request.auth.uid
|
allow update: if resource.data.id == request.auth.uid
|
||||||
&& request.resource.data.diff(resource.data).affectedKeys()
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories']);
|
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'referredByContractId']);
|
||||||
|
// only one referral allowed per user
|
||||||
|
allow update: if resource.data.id == request.auth.uid
|
||||||
|
&& request.resource.data.diff(resource.data).affectedKeys()
|
||||||
|
.hasOnly(['referredByUserId'])
|
||||||
|
&& !("referredByUserId" in resource.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {
|
||||||
|
|
|
@ -68,6 +68,7 @@ export const createNotification = async (
|
||||||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||||
// TODO: move away from sourceContractTitle to sourceTitle
|
// TODO: move away from sourceContractTitle to sourceTitle
|
||||||
sourceContractTitle: sourceContract?.question,
|
sourceContractTitle: sourceContract?.question,
|
||||||
|
// TODO: move away from sourceContractSlug to sourceSlug
|
||||||
sourceContractSlug: sourceContract?.slug,
|
sourceContractSlug: sourceContract?.slug,
|
||||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
||||||
|
@ -252,44 +253,62 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyUserReceivedReferralBonus = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
relatedUserId: string
|
||||||
|
) => {
|
||||||
|
if (shouldGetNotification(relatedUserId, userToReasonTexts))
|
||||||
|
userToReasonTexts[relatedUserId] = {
|
||||||
|
// If the referrer is the market creator, just tell them they joined to bet on their market
|
||||||
|
reason:
|
||||||
|
sourceContract?.creatorId === relatedUserId
|
||||||
|
? 'user_joined_to_bet_on_your_market'
|
||||||
|
: 'you_referred_user',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
if (sourceContract) {
|
if (sourceType === 'follow' && relatedUserId) {
|
||||||
if (
|
|
||||||
sourceType === 'comment' ||
|
|
||||||
sourceType === 'answer' ||
|
|
||||||
(sourceType === 'contract' &&
|
|
||||||
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
|
||||||
) {
|
|
||||||
if (sourceType === 'comment') {
|
|
||||||
if (relatedUserId && relatedSourceType)
|
|
||||||
await notifyRepliedUsers(
|
|
||||||
userToReasonTexts,
|
|
||||||
relatedUserId,
|
|
||||||
relatedSourceType
|
|
||||||
)
|
|
||||||
if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText)
|
|
||||||
}
|
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
|
||||||
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
|
||||||
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
|
||||||
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
|
||||||
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
|
||||||
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
|
||||||
await notifyUsersFollowers(userToReasonTexts)
|
|
||||||
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
|
||||||
force: true,
|
|
||||||
})
|
|
||||||
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
|
||||||
}
|
|
||||||
} else if (sourceType === 'follow' && relatedUserId) {
|
|
||||||
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
||||||
} else if (sourceType === 'group' && relatedUserId) {
|
} else if (sourceType === 'group' && relatedUserId) {
|
||||||
if (sourceUpdateType === 'created')
|
if (sourceUpdateType === 'created')
|
||||||
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
|
||||||
|
} else if (sourceType === 'user' && relatedUserId) {
|
||||||
|
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following functions need sourceContract to be defined.
|
||||||
|
if (!sourceContract) return userToReasonTexts
|
||||||
|
if (
|
||||||
|
sourceType === 'comment' ||
|
||||||
|
sourceType === 'answer' ||
|
||||||
|
(sourceType === 'contract' &&
|
||||||
|
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
||||||
|
) {
|
||||||
|
if (sourceType === 'comment') {
|
||||||
|
if (relatedUserId && relatedSourceType)
|
||||||
|
await notifyRepliedUsers(
|
||||||
|
userToReasonTexts,
|
||||||
|
relatedUserId,
|
||||||
|
relatedSourceType
|
||||||
|
)
|
||||||
|
if (sourceText) await notifyTaggedUsers(userToReasonTexts, sourceText)
|
||||||
|
}
|
||||||
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||||
|
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
||||||
|
await notifyLiquidityProviders(userToReasonTexts, sourceContract)
|
||||||
|
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
|
||||||
|
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
||||||
|
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
||||||
|
await notifyUsersFollowers(userToReasonTexts)
|
||||||
|
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
|
||||||
|
await notifyContractCreator(userToReasonTexts, sourceContract, {
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
|
||||||
|
await notifyContractCreator(userToReasonTexts, sourceContract)
|
||||||
}
|
}
|
||||||
return userToReasonTexts
|
return userToReasonTexts
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export * from './on-unfollow-user'
|
||||||
export * from './on-create-liquidity-provision'
|
export * from './on-create-liquidity-provision'
|
||||||
export * from './on-update-group'
|
export * from './on-update-group'
|
||||||
export * from './on-create-group'
|
export * from './on-create-group'
|
||||||
|
export * from './on-update-user'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
|
107
functions/src/on-update-user.ts
Normal file
107
functions/src/on-update-user.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import { REFERRAL_AMOUNT, User } from '../../common/user'
|
||||||
|
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
import { ReferralTxn } from '../../common/txn'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
export const onUpdateUser = functions.firestore
|
||||||
|
.document('users/{userId}')
|
||||||
|
.onUpdate(async (change, context) => {
|
||||||
|
const prevUser = change.before.data() as User
|
||||||
|
const user = change.after.data() as User
|
||||||
|
const { eventId } = context
|
||||||
|
|
||||||
|
if (prevUser.referredByUserId !== user.referredByUserId) {
|
||||||
|
await handleUserUpdatedReferral(user, eventId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleUserUpdatedReferral(user: User, eventId: string) {
|
||||||
|
// Only create a referral txn if the user has a referredByUserId
|
||||||
|
if (!user.referredByUserId) {
|
||||||
|
console.log(`Not set: referredByUserId ${user.referredByUserId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const referredByUserId = user.referredByUserId
|
||||||
|
|
||||||
|
await firestore.runTransaction(async (transaction) => {
|
||||||
|
// get user that referred this user
|
||||||
|
const referredByUserDoc = firestore.doc(`users/${referredByUserId}`)
|
||||||
|
const referredByUserSnap = await transaction.get(referredByUserDoc)
|
||||||
|
if (!referredByUserSnap.exists) {
|
||||||
|
console.log(`User ${referredByUserId} not found`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const referredByUser = referredByUserSnap.data() as User
|
||||||
|
|
||||||
|
let referredByContract: Contract | undefined = undefined
|
||||||
|
if (user.referredByContractId) {
|
||||||
|
const referredByContractDoc = firestore.doc(
|
||||||
|
`contracts/${user.referredByContractId}`
|
||||||
|
)
|
||||||
|
referredByContract = await transaction
|
||||||
|
.get(referredByContractDoc)
|
||||||
|
.then((snap) => snap.data() as Contract)
|
||||||
|
}
|
||||||
|
console.log(`referredByContract: ${referredByContract}`)
|
||||||
|
|
||||||
|
const txns = (
|
||||||
|
await firestore
|
||||||
|
.collection('txns')
|
||||||
|
.where('toId', '==', referredByUserId)
|
||||||
|
.where('category', '==', 'REFERRAL')
|
||||||
|
.get()
|
||||||
|
).docs.map((txn) => txn.ref)
|
||||||
|
const referralTxns = await transaction.getAll(...txns).catch((err) => {
|
||||||
|
console.error('error getting txns:', err)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
// If the referring user already has a referral txn due to referring this user, halt
|
||||||
|
if (referralTxns.map((txn) => txn.data()?.description).includes(user.id)) {
|
||||||
|
console.log('found referral txn with the same details, aborting')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log('creating referral txns')
|
||||||
|
const fromId = HOUSE_LIQUIDITY_PROVIDER_ID
|
||||||
|
|
||||||
|
// if they're updating their referredId, create a txn for both
|
||||||
|
const txn: ReferralTxn = {
|
||||||
|
id: eventId,
|
||||||
|
createdTime: Date.now(),
|
||||||
|
fromId,
|
||||||
|
fromType: 'BANK',
|
||||||
|
toId: referredByUserId,
|
||||||
|
toType: 'USER',
|
||||||
|
amount: REFERRAL_AMOUNT,
|
||||||
|
token: 'M$',
|
||||||
|
category: 'REFERRAL',
|
||||||
|
description: `Referred new user id: ${user.id} for ${REFERRAL_AMOUNT}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const txnDoc = await firestore.collection(`txns/`).doc(txn.id)
|
||||||
|
await transaction.set(txnDoc, txn)
|
||||||
|
console.log('created referral with txn id:', txn.id)
|
||||||
|
// We're currently not subtracting M$ from the house, not sure if we want to for accounting purposes.
|
||||||
|
transaction.update(referredByUserDoc, {
|
||||||
|
balance: referredByUser.balance + REFERRAL_AMOUNT,
|
||||||
|
totalDeposits: referredByUser.totalDeposits + REFERRAL_AMOUNT,
|
||||||
|
})
|
||||||
|
|
||||||
|
await createNotification(
|
||||||
|
user.id,
|
||||||
|
'user',
|
||||||
|
'updated',
|
||||||
|
user,
|
||||||
|
eventId,
|
||||||
|
txn.amount.toString(),
|
||||||
|
referredByContract,
|
||||||
|
'user',
|
||||||
|
referredByUser.id,
|
||||||
|
referredByContract?.slug,
|
||||||
|
referredByContract?.question
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -29,6 +29,8 @@ import { groupPath } from 'web/lib/firebase/groups'
|
||||||
import { SiteLink } from 'web/components/site-link'
|
import { SiteLink } from 'web/components/site-link'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
import { useGroupsWithContract } from 'web/hooks/use-group'
|
import { useGroupsWithContract } from 'web/hooks/use-group'
|
||||||
|
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
|
||||||
export type ShowTime = 'resolve-date' | 'close-date'
|
export type ShowTime = 'resolve-date' | 'close-date'
|
||||||
|
|
||||||
|
@ -130,6 +132,7 @@ export function ContractDetails(props: {
|
||||||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||||
// Find a group that this contract id is in
|
// Find a group that this contract id is in
|
||||||
const groups = useGroupsWithContract(contract.id)
|
const groups = useGroupsWithContract(contract.id)
|
||||||
|
const user = useUser()
|
||||||
return (
|
return (
|
||||||
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
<Row className="flex-1 flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||||
<Row className="items-center gap-2">
|
<Row className="items-center gap-2">
|
||||||
|
@ -192,6 +195,11 @@ export function ContractDetails(props: {
|
||||||
|
|
||||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||||
</Row>
|
</Row>
|
||||||
|
<ShareIconButton
|
||||||
|
contract={contract}
|
||||||
|
toastClassName={'sm:-left-40 -left-24 min-w-[250%]'}
|
||||||
|
username={user?.username}
|
||||||
|
/>
|
||||||
|
|
||||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
getBinaryProbPercent,
|
getBinaryProbPercent,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { LiquidityPanel } from '../liquidity-panel'
|
import { LiquidityPanel } from '../liquidity-panel'
|
||||||
import { CopyLinkButton } from '../copy-link-button'
|
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { Modal } from '../layout/modal'
|
import { Modal } from '../layout/modal'
|
||||||
import { Row } from '../layout/row'
|
import { Row } from '../layout/row'
|
||||||
|
@ -23,6 +22,9 @@ import { TweetButton } from '../tweet-button'
|
||||||
import { InfoTooltip } from '../info-tooltip'
|
import { InfoTooltip } from '../info-tooltip'
|
||||||
import { TagsInput } from 'web/components/tags-input'
|
import { TagsInput } from 'web/components/tags-input'
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
const { contract, bets } = props
|
const { contract, bets } = props
|
||||||
|
|
||||||
|
@ -48,13 +50,11 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:cursor-pointer hover:bg-gray-100"
|
className={contractDetailsButtonClassName}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
<DotsHorizontalIcon
|
<DotsHorizontalIcon
|
||||||
className={clsx(
|
className={clsx('h-6 w-6 flex-shrink-0')}
|
||||||
'h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500'
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -66,10 +66,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
||||||
<div>Share</div>
|
<div>Share</div>
|
||||||
|
|
||||||
<Row className="justify-start gap-4">
|
<Row className="justify-start gap-4">
|
||||||
<CopyLinkButton
|
|
||||||
contract={contract}
|
|
||||||
toastClassName={'sm:-left-10 -left-4 min-w-[250%]'}
|
|
||||||
/>
|
|
||||||
<TweetButton
|
<TweetButton
|
||||||
className="self-start"
|
className="self-start"
|
||||||
tweetText={getTweetText(contract, false)}
|
tweetText={getTweetText(contract, false)}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function EditGroupButton(props: { group: Group; className?: string }) {
|
||||||
<div className={clsx('flex p-1', className)}>
|
<div className={clsx('flex p-1', className)}>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'btn-ghost cursor-pointer whitespace-nowrap rounded-full text-sm text-white'
|
'btn-ghost cursor-pointer whitespace-nowrap rounded-md p-1 text-sm text-gray-700'
|
||||||
)}
|
)}
|
||||||
onClick={() => updateOpen(!open)}
|
onClick={() => updateOpen(!open)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -91,6 +91,9 @@ export function GroupChat(props: {
|
||||||
setReplyToUsername('')
|
setReplyToUsername('')
|
||||||
inputRef?.focus()
|
inputRef?.focus()
|
||||||
}
|
}
|
||||||
|
function focusInput() {
|
||||||
|
inputRef?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={'flex-1'}>
|
<Col className={'flex-1'}>
|
||||||
|
@ -117,7 +120,13 @@ export function GroupChat(props: {
|
||||||
))}
|
))}
|
||||||
{messages.length === 0 && (
|
{messages.length === 0 && (
|
||||||
<div className="p-2 text-gray-500">
|
<div className="p-2 text-gray-500">
|
||||||
No messages yet. 🦗... Why not say something?
|
No messages yet. Why not{' '}
|
||||||
|
<button
|
||||||
|
className={'cursor-pointer font-bold text-gray-700'}
|
||||||
|
onClick={() => focusInput()}
|
||||||
|
>
|
||||||
|
add one?
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
93
web/components/referrals-button.tsx
Normal file
93
web/components/referrals-button.tsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import { User } from 'common/user'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { prefetchUsers, useUserById } from 'web/hooks/use-user'
|
||||||
|
import { Col } from './layout/col'
|
||||||
|
import { Modal } from './layout/modal'
|
||||||
|
import { Tabs } from './layout/tabs'
|
||||||
|
import { TextButton } from './text-button'
|
||||||
|
import { Row } from 'web/components/layout/row'
|
||||||
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
import { UserLink } from 'web/components/user-page'
|
||||||
|
import { useReferrals } from 'web/hooks/use-referrals'
|
||||||
|
|
||||||
|
export function ReferralsButton(props: { user: User }) {
|
||||||
|
const { user } = props
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const referralIds = useReferrals(user.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextButton onClick={() => setIsOpen(true)}>
|
||||||
|
<span className="font-semibold">{referralIds?.length ?? ''}</span>{' '}
|
||||||
|
Referrals
|
||||||
|
</TextButton>
|
||||||
|
|
||||||
|
<ReferralsDialog
|
||||||
|
user={user}
|
||||||
|
referralIds={referralIds ?? []}
|
||||||
|
isOpen={isOpen}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferralsDialog(props: {
|
||||||
|
user: User
|
||||||
|
referralIds: string[]
|
||||||
|
isOpen: boolean
|
||||||
|
setIsOpen: (isOpen: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { user, referralIds, isOpen, setIsOpen } = props
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
prefetchUsers(referralIds)
|
||||||
|
}, [referralIds])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} setOpen={setIsOpen}>
|
||||||
|
<Col className="rounded bg-white p-6">
|
||||||
|
<div className="p-2 pb-1 text-xl">{user.name}</div>
|
||||||
|
<div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div>
|
||||||
|
<Tabs
|
||||||
|
tabs={[
|
||||||
|
{
|
||||||
|
title: 'Referrals',
|
||||||
|
content: <ReferralsList userIds={referralIds} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReferralsList(props: { userIds: string[] }) {
|
||||||
|
const { userIds } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col className="gap-2">
|
||||||
|
{userIds.length === 0 && (
|
||||||
|
<div className="text-gray-500">No users yet...</div>
|
||||||
|
)}
|
||||||
|
{userIds.map((userId) => (
|
||||||
|
<UserReferralItem key={userId} userId={userId} />
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserReferralItem(props: { userId: string; className?: string }) {
|
||||||
|
const { userId, className } = props
|
||||||
|
const user = useUserById(userId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
|
||||||
|
<Row className="items-center gap-2">
|
||||||
|
<Avatar username={user?.username} avatarUrl={user?.avatarUrl} />
|
||||||
|
{user && <UserLink name={user.name} username={user.username} />}
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
70
web/components/share-icon-button.tsx
Normal file
70
web/components/share-icon-button.tsx
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { ShareIcon } from '@heroicons/react/outline'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { Contract } from 'common/contract'
|
||||||
|
import { copyToClipboard } from 'web/lib/util/copy'
|
||||||
|
import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
|
import { ToastClipboard } from 'web/components/toast-clipboard'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { contractDetailsButtonClassName } from 'web/components/contract/contract-info-dialog'
|
||||||
|
import { Group } from 'common/group'
|
||||||
|
import { groupPath } from 'web/lib/firebase/groups'
|
||||||
|
|
||||||
|
function copyContractWithReferral(contract: Contract, username?: string) {
|
||||||
|
const postFix =
|
||||||
|
username && contract.creatorUsername !== username
|
||||||
|
? '?referrer=' + username
|
||||||
|
: ''
|
||||||
|
copyToClipboard(
|
||||||
|
`https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: if a user arrives at a /group endpoint with a ?referral= query, they'll be added to the group automatically
|
||||||
|
function copyGroupWithReferral(group: Group, username?: string) {
|
||||||
|
const postFix = username ? '?referrer=' + username : ''
|
||||||
|
copyToClipboard(
|
||||||
|
`https://${ENV_CONFIG.domain}${groupPath(group.slug)}${postFix}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShareIconButton(props: {
|
||||||
|
contract?: Contract
|
||||||
|
group?: Group
|
||||||
|
buttonClassName?: string
|
||||||
|
toastClassName?: string
|
||||||
|
username?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
contract,
|
||||||
|
buttonClassName,
|
||||||
|
toastClassName,
|
||||||
|
username,
|
||||||
|
group,
|
||||||
|
children,
|
||||||
|
} = props
|
||||||
|
const [showToast, setShowToast] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative z-10 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
className={clsx(contractDetailsButtonClassName, buttonClassName)}
|
||||||
|
onClick={() => {
|
||||||
|
if (contract) copyContractWithReferral(contract, username)
|
||||||
|
if (group) copyGroupWithReferral(group, username)
|
||||||
|
track('copy share link')
|
||||||
|
setShowToast(true)
|
||||||
|
setTimeout(() => setShowToast(false), 2000)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ShareIcon className="h-[24px] w-5" aria-hidden="true" />
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{showToast && <ToastClipboard className={toastClassName} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ import { FollowersButton, FollowingButton } from './following-button'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
import { FollowButton } from './follow-button'
|
import { FollowButton } from './follow-button'
|
||||||
import { PortfolioMetrics } from 'common/user'
|
import { PortfolioMetrics } from 'common/user'
|
||||||
|
import { ReferralsButton } from 'web/components/referrals-button'
|
||||||
import { GroupsButton } from 'web/components/groups/groups-button'
|
import { GroupsButton } from 'web/components/groups/groups-button'
|
||||||
|
|
||||||
export function UserLink(props: {
|
export function UserLink(props: {
|
||||||
|
@ -194,10 +195,11 @@ export function UserPage(props: {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col className="gap-2 sm:flex-row sm:items-center sm:gap-4">
|
<Col className="flex-wrap gap-2 sm:flex-row sm:items-center sm:gap-4">
|
||||||
<Row className="gap-4">
|
<Row className="gap-4">
|
||||||
<FollowingButton user={user} />
|
<FollowingButton user={user} />
|
||||||
<FollowersButton user={user} />
|
<FollowersButton user={user} />
|
||||||
|
<ReferralsButton user={user} />
|
||||||
<GroupsButton user={user} />
|
<GroupsButton user={user} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ function getAppropriateNotifications(
|
||||||
return notifications.filter(
|
return notifications.filter(
|
||||||
(n) =>
|
(n) =>
|
||||||
n.reason &&
|
n.reason &&
|
||||||
// Show all contract notifications
|
// Show all contract notifications and any that aren't in the above list:
|
||||||
(n.sourceType === 'contract' || !lessPriorityReasons.includes(n.reason))
|
(n.sourceType === 'contract' || !lessPriorityReasons.includes(n.reason))
|
||||||
)
|
)
|
||||||
if (notificationPreferences === 'none') return []
|
if (notificationPreferences === 'none') return []
|
||||||
|
|
12
web/hooks/use-referrals.ts
Normal file
12
web/hooks/use-referrals.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { listenForReferrals } from 'web/lib/firebase/users'
|
||||||
|
|
||||||
|
export const useReferrals = (userId: string | null | undefined) => {
|
||||||
|
const [referralIds, setReferralIds] = useState<string[] | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userId) return listenForReferrals(userId, setReferralIds)
|
||||||
|
}, [userId])
|
||||||
|
|
||||||
|
return referralIds
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import {
|
||||||
updateDoc,
|
updateDoc,
|
||||||
where,
|
where,
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { sortBy } from 'lodash'
|
import { sortBy, uniq } from 'lodash'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { getContractFromId } from './contracts'
|
import { getContractFromId } from './contracts'
|
||||||
import {
|
import {
|
||||||
|
@ -95,6 +95,16 @@ export async function getGroupsWithContractId(
|
||||||
setGroups(await getValues<Group>(q))
|
setGroups(await getValues<Group>(q))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function addUserToGroupViaSlug(groupSlug: string, userId: string) {
|
||||||
|
// get group to get the member ids
|
||||||
|
const group = await getGroupBySlug(groupSlug)
|
||||||
|
if (!group) {
|
||||||
|
console.error(`Group not found: ${groupSlug}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return await joinGroup(group, userId)
|
||||||
|
}
|
||||||
|
|
||||||
export async function joinGroup(group: Group, userId: string): Promise<Group> {
|
export async function joinGroup(group: Group, userId: string): Promise<Group> {
|
||||||
const { memberIds } = group
|
const { memberIds } = group
|
||||||
if (memberIds.includes(userId)) {
|
if (memberIds.includes(userId)) {
|
||||||
|
@ -102,7 +112,7 @@ export async function joinGroup(group: Group, userId: string): Promise<Group> {
|
||||||
}
|
}
|
||||||
const newMemberIds = [...memberIds, userId]
|
const newMemberIds = [...memberIds, userId]
|
||||||
const newGroup = { ...group, memberIds: newMemberIds }
|
const newGroup = { ...group, memberIds: newMemberIds }
|
||||||
await updateGroup(newGroup, { memberIds: newMemberIds })
|
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
||||||
return newGroup
|
return newGroup
|
||||||
}
|
}
|
||||||
export async function leaveGroup(group: Group, userId: string): Promise<Group> {
|
export async function leaveGroup(group: Group, userId: string): Promise<Group> {
|
||||||
|
@ -112,6 +122,6 @@ export async function leaveGroup(group: Group, userId: string): Promise<Group> {
|
||||||
}
|
}
|
||||||
const newMemberIds = memberIds.filter((id) => id !== userId)
|
const newMemberIds = memberIds.filter((id) => id !== userId)
|
||||||
const newGroup = { ...group, memberIds: newMemberIds }
|
const newGroup = { ...group, memberIds: newMemberIds }
|
||||||
await updateGroup(newGroup, { memberIds: newMemberIds })
|
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
||||||
return newGroup
|
return newGroup
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,10 @@ import { feed } from 'common/feed'
|
||||||
import { CATEGORY_LIST } from 'common/categories'
|
import { CATEGORY_LIST } from 'common/categories'
|
||||||
import { safeLocalStorage } from '../util/local'
|
import { safeLocalStorage } from '../util/local'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
|
import { addUserToGroupViaSlug } from 'web/lib/firebase/groups'
|
||||||
|
import { removeUndefinedProps } from 'common/util/object'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
|
||||||
export const users = coll<User>('users')
|
export const users = coll<User>('users')
|
||||||
export const privateUsers = coll<PrivateUser>('private-users')
|
export const privateUsers = coll<PrivateUser>('private-users')
|
||||||
|
@ -90,12 +94,92 @@ export function listenForPrivateUser(
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
const CACHED_USER_KEY = 'CACHED_USER_KEY'
|
||||||
|
const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
|
||||||
|
const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'
|
||||||
|
const CACHED_REFERRAL_GROUP_SLUG_KEY = 'CACHED_REFERRAL_GROUP_KEY'
|
||||||
|
|
||||||
// used to avoid weird race condition
|
// used to avoid weird race condition
|
||||||
let createUserPromise: Promise<User | null> | undefined = undefined
|
let createUserPromise: Promise<User | null> | undefined = undefined
|
||||||
|
|
||||||
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
|
const warmUpCreateUser = throttle(createUser, 5000 /* ms */)
|
||||||
|
|
||||||
|
export function writeReferralInfo(
|
||||||
|
defaultReferrerUsername: string,
|
||||||
|
contractId?: string,
|
||||||
|
referralUsername?: string,
|
||||||
|
groupSlug?: string
|
||||||
|
) {
|
||||||
|
const local = safeLocalStorage()
|
||||||
|
const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)
|
||||||
|
// Write the first referral username we see.
|
||||||
|
if (!cachedReferralUser)
|
||||||
|
local?.setItem(
|
||||||
|
CACHED_REFERRAL_USERNAME_KEY,
|
||||||
|
referralUsername || defaultReferrerUsername
|
||||||
|
)
|
||||||
|
|
||||||
|
// If an explicit referral query is passed, overwrite the cached referral username.
|
||||||
|
if (referralUsername)
|
||||||
|
local?.setItem(CACHED_REFERRAL_USERNAME_KEY, referralUsername)
|
||||||
|
|
||||||
|
// Always write the most recent explicit group invite query value
|
||||||
|
if (groupSlug) local?.setItem(CACHED_REFERRAL_GROUP_SLUG_KEY, groupSlug)
|
||||||
|
|
||||||
|
// Write the first contract id that we see.
|
||||||
|
const cachedReferralContract = local?.getItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||||
|
if (!cachedReferralContract && contractId)
|
||||||
|
local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setCachedReferralInfoForUser(user: User | null) {
|
||||||
|
if (!user || user.referredByUserId) return
|
||||||
|
// if the user wasn't created in the last minute, don't bother
|
||||||
|
const now = dayjs().utc()
|
||||||
|
const userCreatedTime = dayjs(user.createdTime)
|
||||||
|
if (now.diff(userCreatedTime, 'minute') > 1) return
|
||||||
|
|
||||||
|
const local = safeLocalStorage()
|
||||||
|
const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)
|
||||||
|
const cachedReferralContractId = local?.getItem(
|
||||||
|
CACHED_REFERRAL_CONTRACT_ID_KEY
|
||||||
|
)
|
||||||
|
const cachedReferralGroupSlug = local?.getItem(CACHED_REFERRAL_GROUP_SLUG_KEY)
|
||||||
|
|
||||||
|
// get user via username
|
||||||
|
if (cachedReferralUsername)
|
||||||
|
getUserByUsername(cachedReferralUsername).then((referredByUser) => {
|
||||||
|
if (!referredByUser) return
|
||||||
|
// update user's referralId
|
||||||
|
updateUser(
|
||||||
|
user.id,
|
||||||
|
removeUndefinedProps({
|
||||||
|
referredByUserId: referredByUser.id,
|
||||||
|
referredByContractId: cachedReferralContractId
|
||||||
|
? cachedReferralContractId
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
console.log('error setting referral details', err)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
track('Referral', {
|
||||||
|
userId: user.id,
|
||||||
|
referredByUserId: referredByUser.id,
|
||||||
|
referredByContractId: cachedReferralContractId,
|
||||||
|
referredByGroupSlug: cachedReferralGroupSlug,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cachedReferralGroupSlug)
|
||||||
|
addUserToGroupViaSlug(cachedReferralGroupSlug, user.id)
|
||||||
|
|
||||||
|
local?.removeItem(CACHED_REFERRAL_GROUP_SLUG_KEY)
|
||||||
|
local?.removeItem(CACHED_REFERRAL_USERNAME_KEY)
|
||||||
|
local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
export function listenForLogin(onUser: (user: User | null) => void) {
|
export function listenForLogin(onUser: (user: User | null) => void) {
|
||||||
const local = safeLocalStorage()
|
const local = safeLocalStorage()
|
||||||
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
const cachedUser = local?.getItem(CACHED_USER_KEY)
|
||||||
|
@ -119,6 +203,7 @@ export function listenForLogin(onUser: (user: User | null) => void) {
|
||||||
// Persist to local storage, to reduce login blink next time.
|
// Persist to local storage, to reduce login blink next time.
|
||||||
// Note: Cap on localStorage size is ~5mb
|
// Note: Cap on localStorage size is ~5mb
|
||||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||||
|
setCachedReferralInfoForUser(user)
|
||||||
} else {
|
} else {
|
||||||
// User logged out; reset to null
|
// User logged out; reset to null
|
||||||
onUser(null)
|
onUser(null)
|
||||||
|
@ -279,3 +364,22 @@ export function listenForFollowers(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
export function listenForReferrals(
|
||||||
|
userId: string,
|
||||||
|
setReferralIds: (referralIds: string[]) => void
|
||||||
|
) {
|
||||||
|
const referralsQuery = query(
|
||||||
|
collection(db, 'users'),
|
||||||
|
where('referredByUserId', '==', userId)
|
||||||
|
)
|
||||||
|
return onSnapshot(
|
||||||
|
referralsQuery,
|
||||||
|
{ includeMetadataChanges: true },
|
||||||
|
(snapshot) => {
|
||||||
|
if (snapshot.metadata.fromCache) return
|
||||||
|
|
||||||
|
const values = snapshot.docs.map((doc) => doc.ref.id)
|
||||||
|
setReferralIds(filterDefined(values))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { useUser } from 'web/hooks/use-user'
|
||||||
import { ResolutionPanel } from 'web/components/resolution-panel'
|
import { ResolutionPanel } from 'web/components/resolution-panel'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { listUsers, User } from 'web/lib/firebase/users'
|
import { listUsers, User, writeReferralInfo } from 'web/lib/firebase/users'
|
||||||
import {
|
import {
|
||||||
Contract,
|
Contract,
|
||||||
getContractFromSlug,
|
getContractFromSlug,
|
||||||
|
@ -42,6 +42,7 @@ import { useBets } from 'web/hooks/use-bets'
|
||||||
import { AlertBox } from 'web/components/alert-box'
|
import { AlertBox } from 'web/components/alert-box'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
|
@ -150,6 +151,16 @@ export function ContractPageContent(
|
||||||
|
|
||||||
const ogCardProps = getOpenGraphProps(contract)
|
const ogCardProps = getOpenGraphProps(contract)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { referrer } = router.query as {
|
||||||
|
referrer?: string
|
||||||
|
}
|
||||||
|
if (!user && router.isReady)
|
||||||
|
writeReferralInfo(contract.creatorUsername, contract.id, referrer)
|
||||||
|
}, [user, contract, router])
|
||||||
|
|
||||||
const rightSidebar = hasSidePanel ? (
|
const rightSidebar = hasSidePanel ? (
|
||||||
<Col className="gap-4">
|
<Col className="gap-4">
|
||||||
{allowTrade &&
|
{allowTrade &&
|
||||||
|
|
|
@ -13,7 +13,12 @@ import {
|
||||||
} from 'web/lib/firebase/groups'
|
} from 'web/lib/firebase/groups'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
import {
|
||||||
|
firebaseLogin,
|
||||||
|
getUser,
|
||||||
|
User,
|
||||||
|
writeReferralInfo,
|
||||||
|
} from 'web/lib/firebase/users'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
@ -40,6 +45,9 @@ import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useCommentsOnGroup } from 'web/hooks/use-comments'
|
import { useCommentsOnGroup } from 'web/hooks/use-comments'
|
||||||
import ShortToggle from 'web/components/widgets/short-toggle'
|
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||||
|
import { ShareIconButton } from 'web/components/share-icon-button'
|
||||||
|
import { REFERRAL_AMOUNT } from 'common/user'
|
||||||
|
import { SiteLink } from 'web/components/site-link'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -150,6 +158,14 @@ export default function GroupPage(props: {
|
||||||
}, [group])
|
}, [group])
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
useEffect(() => {
|
||||||
|
const { referrer } = router.query as {
|
||||||
|
referrer?: string
|
||||||
|
}
|
||||||
|
if (!user && router.isReady)
|
||||||
|
writeReferralInfo(creator.username, undefined, referrer, group?.slug)
|
||||||
|
}, [user, creator, group, router])
|
||||||
|
|
||||||
if (group === null || !groupSubpages.includes(page) || slugs[2]) {
|
if (group === null || !groupSubpages.includes(page) || slugs[2]) {
|
||||||
return <Custom404 />
|
return <Custom404 />
|
||||||
}
|
}
|
||||||
|
@ -257,7 +273,13 @@ export default function GroupPage(props: {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 text-gray-500">
|
<div className="p-2 text-gray-500">
|
||||||
No questions yet. 🦗... Why not add one?
|
No questions yet. Why not{' '}
|
||||||
|
<SiteLink
|
||||||
|
href={`/create/?groupId=${group.id}`}
|
||||||
|
className={'font-bold text-gray-700'}
|
||||||
|
>
|
||||||
|
add one?
|
||||||
|
</SiteLink>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
|
@ -321,18 +343,17 @@ function GroupOverview(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Row className="items-center justify-end rounded-t bg-indigo-500 px-4 py-3 text-sm text-white">
|
|
||||||
<Row className="flex-1 justify-start">About {group.name}</Row>
|
|
||||||
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
|
||||||
</Row>
|
|
||||||
<Col className="gap-2 rounded-b bg-white p-4">
|
<Col className="gap-2 rounded-b bg-white p-4">
|
||||||
<Row>
|
<Row className={'flex-wrap justify-between'}>
|
||||||
<div className="mr-1 text-gray-500">Created by</div>
|
<div className={'inline-flex items-center'}>
|
||||||
<UserLink
|
<div className="mr-1 text-gray-500">Created by</div>
|
||||||
className="text-neutral"
|
<UserLink
|
||||||
name={creator.name}
|
className="text-neutral"
|
||||||
username={creator.username}
|
name={creator.name}
|
||||||
/>
|
username={creator.username}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
||||||
</Row>
|
</Row>
|
||||||
<Row className={'items-center gap-1'}>
|
<Row className={'items-center gap-1'}>
|
||||||
<span className={'text-gray-500'}>Membership</span>
|
<span className={'text-gray-500'}>Membership</span>
|
||||||
|
@ -352,6 +373,20 @@ function GroupOverview(props: {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
|
{anyoneCanJoin && user && (
|
||||||
|
<Row className={'flex-wrap items-center gap-1'}>
|
||||||
|
<span className={'text-gray-500'}>Sharing</span>
|
||||||
|
<ShareIconButton
|
||||||
|
group={group}
|
||||||
|
username={user.username}
|
||||||
|
buttonClassName={'hover:bg-gray-300 mt-1 !text-gray-700'}
|
||||||
|
>
|
||||||
|
<span className={'mx-2'}>
|
||||||
|
Invite a friend and get M${REFERRAL_AMOUNT} if they sign up!
|
||||||
|
</span>
|
||||||
|
</ShareIconButton>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,9 +14,6 @@ import { Title } from 'web/components/title'
|
||||||
import { doc, updateDoc } from 'firebase/firestore'
|
import { doc, updateDoc } from 'firebase/firestore'
|
||||||
import { db } from 'web/lib/firebase/init'
|
import { db } from 'web/lib/firebase/init'
|
||||||
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
||||||
import { Answer } from 'common/answer'
|
|
||||||
import { Comment } from 'web/lib/firebase/comments'
|
|
||||||
import { getValue } from 'web/lib/firebase/utils'
|
|
||||||
import Custom404 from 'web/pages/404'
|
import Custom404 from 'web/pages/404'
|
||||||
import { UserLink } from 'web/components/user-page'
|
import { UserLink } from 'web/components/user-page'
|
||||||
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
||||||
|
@ -38,7 +35,6 @@ import {
|
||||||
NotificationGroup,
|
NotificationGroup,
|
||||||
usePreferredGroupedNotifications,
|
usePreferredGroupedNotifications,
|
||||||
} from 'web/hooks/use-notifications'
|
} from 'web/hooks/use-notifications'
|
||||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
|
||||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
import { CheckIcon, 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'
|
||||||
|
@ -182,7 +178,7 @@ function NotificationGroupItem(props: {
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
const { notificationGroup, className } = props
|
const { notificationGroup, className } = props
|
||||||
const { sourceContractId, notifications } = notificationGroup
|
const { notifications } = notificationGroup
|
||||||
const {
|
const {
|
||||||
sourceContractTitle,
|
sourceContractTitle,
|
||||||
sourceContractSlug,
|
sourceContractSlug,
|
||||||
|
@ -191,28 +187,6 @@ function NotificationGroupItem(props: {
|
||||||
const numSummaryLines = 3
|
const numSummaryLines = 3
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
const [contract, setContract] = useState<Contract | undefined>(undefined)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
sourceContractTitle &&
|
|
||||||
sourceContractSlug &&
|
|
||||||
sourceContractCreatorUsername
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (sourceContractId) {
|
|
||||||
getContractFromId(sourceContractId)
|
|
||||||
.then((contract) => {
|
|
||||||
if (contract) setContract(contract)
|
|
||||||
})
|
|
||||||
.catch((e) => console.log(e))
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
sourceContractCreatorUsername,
|
|
||||||
sourceContractId,
|
|
||||||
sourceContractSlug,
|
|
||||||
sourceContractTitle,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotificationsAsSeen(notifications)
|
setNotificationsAsSeen(notifications)
|
||||||
|
@ -240,20 +214,20 @@ function NotificationGroupItem(props: {
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||||
>
|
>
|
||||||
{sourceContractTitle || contract ? (
|
{sourceContractTitle ? (
|
||||||
<span>
|
<span>
|
||||||
{'Activity on '}
|
{'Activity on '}
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
sourceContractCreatorUsername
|
sourceContractCreatorUsername
|
||||||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
: ''
|
||||||
}
|
}
|
||||||
className={
|
className={
|
||||||
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{sourceContractTitle || contract?.question}
|
{sourceContractTitle}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -306,6 +280,7 @@ function NotificationGroupItem(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: where should we put referral bonus notifications?
|
||||||
function NotificationSettings() {
|
function NotificationSettings() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const [notificationSettings, setNotificationSettings] =
|
const [notificationSettings, setNotificationSettings] =
|
||||||
|
@ -455,6 +430,10 @@ function NotificationSettings() {
|
||||||
highlight={notificationSettings !== 'none'}
|
highlight={notificationSettings !== 'none'}
|
||||||
label={"Activity on questions you're betting on"}
|
label={"Activity on questions you're betting on"}
|
||||||
/>
|
/>
|
||||||
|
<NotificationSettingLine
|
||||||
|
highlight={notificationSettings !== 'none'}
|
||||||
|
label={"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"}
|
||||||
highlight={notificationSettings === 'all'}
|
highlight={notificationSettings === 'all'}
|
||||||
|
@ -515,7 +494,6 @@ function NotificationItem(props: {
|
||||||
const { notification, justSummary } = props
|
const { notification, justSummary } = props
|
||||||
const {
|
const {
|
||||||
sourceType,
|
sourceType,
|
||||||
sourceContractId,
|
|
||||||
sourceId,
|
sourceId,
|
||||||
sourceUserName,
|
sourceUserName,
|
||||||
sourceUserAvatarUrl,
|
sourceUserAvatarUrl,
|
||||||
|
@ -534,60 +512,15 @@ function NotificationItem(props: {
|
||||||
|
|
||||||
const [defaultNotificationText, setDefaultNotificationText] =
|
const [defaultNotificationText, setDefaultNotificationText] =
|
||||||
useState<string>('')
|
useState<string>('')
|
||||||
const [contract, setContract] = useState<Contract | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!sourceContractId ||
|
|
||||||
(sourceContractSlug && sourceContractCreatorUsername)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
getContractFromId(sourceContractId)
|
|
||||||
.then((contract) => {
|
|
||||||
if (contract) setContract(contract)
|
|
||||||
})
|
|
||||||
.catch((e) => console.log(e))
|
|
||||||
}, [
|
|
||||||
sourceContractCreatorUsername,
|
|
||||||
sourceContractId,
|
|
||||||
sourceContractSlug,
|
|
||||||
sourceContractTitle,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sourceText) {
|
if (sourceText) {
|
||||||
setDefaultNotificationText(sourceText)
|
setDefaultNotificationText(sourceText)
|
||||||
} else if (!contract || !sourceContractId || !sourceId) return
|
|
||||||
else if (
|
|
||||||
sourceType === 'answer' ||
|
|
||||||
sourceType === 'comment' ||
|
|
||||||
sourceType === 'contract'
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
parseOldStyleNotificationText(
|
|
||||||
sourceId,
|
|
||||||
sourceContractId,
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
setDefaultNotificationText,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
} else if (reasonText) {
|
} else if (reasonText) {
|
||||||
// Handle arbitrary notifications with reason text here.
|
// Handle arbitrary notifications with reason text here.
|
||||||
setDefaultNotificationText(reasonText)
|
setDefaultNotificationText(reasonText)
|
||||||
}
|
}
|
||||||
}, [
|
}, [reasonText, sourceText])
|
||||||
contract,
|
|
||||||
reasonText,
|
|
||||||
sourceContractId,
|
|
||||||
sourceId,
|
|
||||||
sourceText,
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotificationsAsSeen([notification])
|
setNotificationsAsSeen([notification])
|
||||||
|
@ -596,14 +529,16 @@ function NotificationItem(props: {
|
||||||
function getSourceUrl() {
|
function getSourceUrl() {
|
||||||
if (sourceType === 'follow') return `/${sourceUserUsername}`
|
if (sourceType === 'follow') return `/${sourceUserUsername}`
|
||||||
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
|
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||||
|
if (
|
||||||
|
sourceContractCreatorUsername &&
|
||||||
|
sourceContractSlug &&
|
||||||
|
sourceType === 'user'
|
||||||
|
)
|
||||||
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||||
sourceId ?? ''
|
sourceId ?? ''
|
||||||
)}`
|
)}`
|
||||||
if (!contract) return ''
|
|
||||||
return `/${contract.creatorUsername}/${
|
|
||||||
contract.slug
|
|
||||||
}#${getSourceIdForLinkComponent(sourceId ?? '')}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSourceIdForLinkComponent(sourceId: string) {
|
function getSourceIdForLinkComponent(sourceId: string) {
|
||||||
|
@ -619,38 +554,6 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseOldStyleNotificationText(
|
|
||||||
sourceId: string,
|
|
||||||
sourceContractId: string,
|
|
||||||
sourceType: 'answer' | 'comment' | 'contract',
|
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
|
||||||
setText: (text: string) => void,
|
|
||||||
contract: Contract
|
|
||||||
) {
|
|
||||||
if (sourceType === 'contract') {
|
|
||||||
if (
|
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
) &&
|
|
||||||
contract.resolution
|
|
||||||
)
|
|
||||||
setText(contract.resolution)
|
|
||||||
else setText(contract.question)
|
|
||||||
} else if (sourceType === 'answer') {
|
|
||||||
const answer = await getValue<Answer>(
|
|
||||||
doc(db, `contracts/${sourceContractId}/answers/`, sourceId)
|
|
||||||
)
|
|
||||||
setText(answer?.text ?? '')
|
|
||||||
} else {
|
|
||||||
const comment = await getValue<Comment>(
|
|
||||||
doc(db, `contracts/${sourceContractId}/comments/`, sourceId)
|
|
||||||
)
|
|
||||||
setText(comment?.text ?? '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (justSummary) {
|
if (justSummary) {
|
||||||
return (
|
return (
|
||||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||||
|
@ -669,13 +572,13 @@ function NotificationItem(props: {
|
||||||
sourceType,
|
sourceType,
|
||||||
reason,
|
reason,
|
||||||
sourceUpdateType,
|
sourceUpdateType,
|
||||||
contract,
|
undefined,
|
||||||
true
|
true
|
||||||
).replace(' on', '')}
|
).replace(' on', '')}
|
||||||
</span>
|
</span>
|
||||||
<div className={'ml-1 text-black'}>
|
<div className={'ml-1 text-black'}>
|
||||||
<NotificationTextLabel
|
<NotificationTextLabel
|
||||||
contract={contract}
|
contract={null}
|
||||||
defaultText={defaultNotificationText}
|
defaultText={defaultNotificationText}
|
||||||
className={'line-clamp-1'}
|
className={'line-clamp-1'}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
|
@ -717,7 +620,9 @@ function NotificationItem(props: {
|
||||||
sourceType,
|
sourceType,
|
||||||
reason,
|
reason,
|
||||||
sourceUpdateType,
|
sourceUpdateType,
|
||||||
contract
|
undefined,
|
||||||
|
false,
|
||||||
|
sourceSlug
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
|
@ -725,13 +630,13 @@ function NotificationItem(props: {
|
||||||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||||
: sourceType === 'group' && sourceSlug
|
: sourceType === 'group' && sourceSlug
|
||||||
? `${groupPath(sourceSlug)}`
|
? `${groupPath(sourceSlug)}`
|
||||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
: ''
|
||||||
}
|
}
|
||||||
className={
|
className={
|
||||||
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{contract?.question || sourceContractTitle || sourceTitle}
|
{sourceContractTitle || sourceTitle}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -752,7 +657,7 @@ function NotificationItem(props: {
|
||||||
</Row>
|
</Row>
|
||||||
<div className={'mt-1 ml-1 md:text-base'}>
|
<div className={'mt-1 ml-1 md:text-base'}>
|
||||||
<NotificationTextLabel
|
<NotificationTextLabel
|
||||||
contract={contract}
|
contract={null}
|
||||||
defaultText={defaultNotificationText}
|
defaultText={defaultNotificationText}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
/>
|
/>
|
||||||
|
@ -811,6 +716,16 @@ function NotificationTextLabel(props: {
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (sourceType === 'user' && sourceText) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
As a thank you, we sent you{' '}
|
||||||
|
<span className="text-primary">
|
||||||
|
{formatMoney(parseInt(sourceText))}
|
||||||
|
</span>
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
)
|
||||||
} else if (sourceType === 'liquidity' && sourceText) {
|
} else if (sourceType === 'liquidity' && sourceText) {
|
||||||
return (
|
return (
|
||||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||||
|
@ -829,7 +744,8 @@ function getReasonForShowingNotification(
|
||||||
reason: notification_reason_types,
|
reason: notification_reason_types,
|
||||||
sourceUpdateType: notification_source_update_types | undefined,
|
sourceUpdateType: notification_source_update_types | undefined,
|
||||||
contract: Contract | undefined | null,
|
contract: Contract | undefined | null,
|
||||||
simple?: boolean
|
simple?: boolean,
|
||||||
|
sourceSlug?: string
|
||||||
) {
|
) {
|
||||||
let reasonText: string
|
let reasonText: string
|
||||||
switch (source) {
|
switch (source) {
|
||||||
|
@ -883,6 +799,12 @@ function getReasonForShowingNotification(
|
||||||
case 'group':
|
case 'group':
|
||||||
reasonText = 'added you to the group'
|
reasonText = 'added you to the group'
|
||||||
break
|
break
|
||||||
|
case 'user':
|
||||||
|
if (sourceSlug && reason === 'user_joined_to_bet_on_your_market')
|
||||||
|
reasonText = 'joined to bet on your market'
|
||||||
|
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||||
|
else reasonText = 'joined because of you'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
reasonText = ''
|
reasonText = ''
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user