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'
|
||||
| 'admin_message'
|
||||
| 'group'
|
||||
| 'user'
|
||||
|
||||
export type notification_source_update_types =
|
||||
| 'created'
|
||||
|
@ -53,3 +54,5 @@ export type notification_reason_types =
|
|||
| 'on_new_follow'
|
||||
| 'you_follow_user'
|
||||
| '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
|
||||
// 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'
|
||||
|
||||
export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||
|
@ -16,7 +16,7 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' // | 'BET'
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' // | 'BET'
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
|
@ -46,6 +46,13 @@ type Manalink = {
|
|||
category: 'MANALINK'
|
||||
}
|
||||
|
||||
type Referral = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'REFERRAL'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
export type TipTxn = Txn & Tip
|
||||
export type ManalinkTxn = Txn & Manalink
|
||||
export type ReferralTxn = Txn & Referral
|
||||
|
|
|
@ -33,11 +33,14 @@ export type User = {
|
|||
followerCountCached: number
|
||||
|
||||
followedCategories?: string[]
|
||||
|
||||
referredByUserId?: string
|
||||
referredByContractId?: string
|
||||
}
|
||||
|
||||
export const STARTING_BALANCE = 1000
|
||||
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 = {
|
||||
id: string // same as User.id
|
||||
username: string // denormalized from User
|
||||
|
|
|
@ -20,7 +20,12 @@ service cloud.firestore {
|
|||
allow read;
|
||||
allow update: if resource.data.id == request.auth.uid
|
||||
&& 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} {
|
||||
|
|
|
@ -68,6 +68,7 @@ export const createNotification = async (
|
|||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||
// TODO: move away from sourceContractTitle to sourceTitle
|
||||
sourceContractTitle: sourceContract?.question,
|
||||
// TODO: move away from sourceContractSlug to sourceSlug
|
||||
sourceContractSlug: sourceContract?.slug,
|
||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||
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 userToReasonTexts: user_to_reason_texts = {}
|
||||
// The following functions modify the userToReasonTexts object in place.
|
||||
if (sourceContract) {
|
||||
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) {
|
||||
if (sourceType === 'follow' && relatedUserId) {
|
||||
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
||||
} else if (sourceType === 'group' && relatedUserId) {
|
||||
if (sourceUpdateType === 'created')
|
||||
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
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export * from './on-unfollow-user'
|
|||
export * from './on-create-liquidity-provision'
|
||||
export * from './on-update-group'
|
||||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
|
||||
// v2
|
||||
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 { DAY_MS } from 'common/util/time'
|
||||
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'
|
||||
|
||||
|
@ -130,6 +132,7 @@ export function ContractDetails(props: {
|
|||
const { volumeLabel, resolvedDate } = contractMetrics(contract)
|
||||
// Find a group that this contract id is in
|
||||
const groups = useGroupsWithContract(contract.id)
|
||||
const user = useUser()
|
||||
return (
|
||||
<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">
|
||||
|
@ -192,6 +195,11 @@ export function ContractDetails(props: {
|
|||
|
||||
<div className="whitespace-nowrap">{volumeLabel}</div>
|
||||
</Row>
|
||||
<ShareIconButton
|
||||
contract={contract}
|
||||
toastClassName={'sm:-left-40 -left-24 min-w-[250%]'}
|
||||
username={user?.username}
|
||||
/>
|
||||
|
||||
{!disabled && <ContractInfoDialog contract={contract} bets={bets} />}
|
||||
</Row>
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
getBinaryProbPercent,
|
||||
} from 'web/lib/firebase/contracts'
|
||||
import { LiquidityPanel } from '../liquidity-panel'
|
||||
import { CopyLinkButton } from '../copy-link-button'
|
||||
import { Col } from '../layout/col'
|
||||
import { Modal } from '../layout/modal'
|
||||
import { Row } from '../layout/row'
|
||||
|
@ -23,6 +22,9 @@ import { TweetButton } from '../tweet-button'
|
|||
import { InfoTooltip } from '../info-tooltip'
|
||||
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[] }) {
|
||||
const { contract, bets } = props
|
||||
|
||||
|
@ -48,13 +50,11 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
return (
|
||||
<>
|
||||
<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)}
|
||||
>
|
||||
<DotsHorizontalIcon
|
||||
className={clsx(
|
||||
'h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500'
|
||||
)}
|
||||
className={clsx('h-6 w-6 flex-shrink-0')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
@ -66,10 +66,6 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) {
|
|||
<div>Share</div>
|
||||
|
||||
<Row className="justify-start gap-4">
|
||||
<CopyLinkButton
|
||||
contract={contract}
|
||||
toastClassName={'sm:-left-10 -left-4 min-w-[250%]'}
|
||||
/>
|
||||
<TweetButton
|
||||
className="self-start"
|
||||
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(
|
||||
'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)}
|
||||
>
|
||||
|
|
|
@ -91,6 +91,9 @@ export function GroupChat(props: {
|
|||
setReplyToUsername('')
|
||||
inputRef?.focus()
|
||||
}
|
||||
function focusInput() {
|
||||
inputRef?.focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<Col className={'flex-1'}>
|
||||
|
@ -117,7 +120,13 @@ export function GroupChat(props: {
|
|||
))}
|
||||
{messages.length === 0 && (
|
||||
<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>
|
||||
)}
|
||||
</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 { FollowButton } from './follow-button'
|
||||
import { PortfolioMetrics } from 'common/user'
|
||||
import { ReferralsButton } from 'web/components/referrals-button'
|
||||
import { GroupsButton } from 'web/components/groups/groups-button'
|
||||
|
||||
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">
|
||||
<FollowingButton user={user} />
|
||||
<FollowersButton user={user} />
|
||||
<ReferralsButton user={user} />
|
||||
<GroupsButton user={user} />
|
||||
</Row>
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ function getAppropriateNotifications(
|
|||
return notifications.filter(
|
||||
(n) =>
|
||||
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))
|
||||
)
|
||||
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,
|
||||
where,
|
||||
} from 'firebase/firestore'
|
||||
import { sortBy } from 'lodash'
|
||||
import { sortBy, uniq } from 'lodash'
|
||||
import { Group } from 'common/group'
|
||||
import { getContractFromId } from './contracts'
|
||||
import {
|
||||
|
@ -95,6 +95,16 @@ export async function getGroupsWithContractId(
|
|||
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> {
|
||||
const { memberIds } = group
|
||||
if (memberIds.includes(userId)) {
|
||||
|
@ -102,7 +112,7 @@ export async function joinGroup(group: Group, userId: string): Promise<Group> {
|
|||
}
|
||||
const newMemberIds = [...memberIds, userId]
|
||||
const newGroup = { ...group, memberIds: newMemberIds }
|
||||
await updateGroup(newGroup, { memberIds: newMemberIds })
|
||||
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
||||
return newGroup
|
||||
}
|
||||
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 newGroup = { ...group, memberIds: newMemberIds }
|
||||
await updateGroup(newGroup, { memberIds: newMemberIds })
|
||||
await updateGroup(newGroup, { memberIds: uniq(newMemberIds) })
|
||||
return newGroup
|
||||
}
|
||||
|
|
|
@ -36,6 +36,10 @@ import { feed } from 'common/feed'
|
|||
import { CATEGORY_LIST } from 'common/categories'
|
||||
import { safeLocalStorage } from '../util/local'
|
||||
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 privateUsers = coll<PrivateUser>('private-users')
|
||||
|
@ -90,12 +94,92 @@ export function listenForPrivateUser(
|
|||
}
|
||||
|
||||
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
|
||||
let createUserPromise: Promise<User | null> | undefined = undefined
|
||||
|
||||
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) {
|
||||
const local = safeLocalStorage()
|
||||
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.
|
||||
// Note: Cap on localStorage size is ~5mb
|
||||
local?.setItem(CACHED_USER_KEY, JSON.stringify(user))
|
||||
setCachedReferralInfoForUser(user)
|
||||
} else {
|
||||
// User logged out; reset to 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 { Title } from 'web/components/title'
|
||||
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 {
|
||||
Contract,
|
||||
getContractFromSlug,
|
||||
|
@ -42,6 +42,7 @@ import { useBets } from 'web/hooks/use-bets'
|
|||
import { AlertBox } from 'web/components/alert-box'
|
||||
import { useTracking } from 'web/hooks/use-tracking'
|
||||
import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useLiquidity } from 'web/hooks/use-liquidity'
|
||||
|
||||
export const getStaticProps = fromPropz(getStaticPropz)
|
||||
|
@ -150,6 +151,16 @@ export function ContractPageContent(
|
|||
|
||||
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 ? (
|
||||
<Col className="gap-4">
|
||||
{allowTrade &&
|
||||
|
|
|
@ -13,7 +13,12 @@ import {
|
|||
} from 'web/lib/firebase/groups'
|
||||
import { Row } from 'web/components/layout/row'
|
||||
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 { Col } from 'web/components/layout/col'
|
||||
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 { useCommentsOnGroup } from 'web/hooks/use-comments'
|
||||
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 async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||
|
@ -150,6 +158,14 @@ export default function GroupPage(props: {
|
|||
}, [group])
|
||||
|
||||
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]) {
|
||||
return <Custom404 />
|
||||
}
|
||||
|
@ -257,7 +273,13 @@ export default function GroupPage(props: {
|
|||
</>
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
) : (
|
||||
|
@ -321,18 +343,17 @@ function GroupOverview(props: {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<Row>
|
||||
<div className="mr-1 text-gray-500">Created by</div>
|
||||
<UserLink
|
||||
className="text-neutral"
|
||||
name={creator.name}
|
||||
username={creator.username}
|
||||
/>
|
||||
<Row className={'flex-wrap justify-between'}>
|
||||
<div className={'inline-flex items-center'}>
|
||||
<div className="mr-1 text-gray-500">Created by</div>
|
||||
<UserLink
|
||||
className="text-neutral"
|
||||
name={creator.name}
|
||||
username={creator.username}
|
||||
/>
|
||||
</div>
|
||||
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
||||
</Row>
|
||||
<Row className={'items-center gap-1'}>
|
||||
<span className={'text-gray-500'}>Membership</span>
|
||||
|
@ -352,6 +373,20 @@ function GroupOverview(props: {
|
|||
</span>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
|
|
|
@ -14,9 +14,6 @@ import { Title } from 'web/components/title'
|
|||
import { doc, updateDoc } from 'firebase/firestore'
|
||||
import { db } from 'web/lib/firebase/init'
|
||||
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 { UserLink } from 'web/components/user-page'
|
||||
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
||||
|
@ -38,7 +35,6 @@ import {
|
|||
NotificationGroup,
|
||||
usePreferredGroupedNotifications,
|
||||
} from 'web/hooks/use-notifications'
|
||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
||||
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
||||
import toast from 'react-hot-toast'
|
||||
import { formatMoney } from 'common/util/format'
|
||||
|
@ -182,7 +178,7 @@ function NotificationGroupItem(props: {
|
|||
className?: string
|
||||
}) {
|
||||
const { notificationGroup, className } = props
|
||||
const { sourceContractId, notifications } = notificationGroup
|
||||
const { notifications } = notificationGroup
|
||||
const {
|
||||
sourceContractTitle,
|
||||
sourceContractSlug,
|
||||
|
@ -191,28 +187,6 @@ function NotificationGroupItem(props: {
|
|||
const numSummaryLines = 3
|
||||
|
||||
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(() => {
|
||||
setNotificationsAsSeen(notifications)
|
||||
|
@ -240,20 +214,20 @@ function NotificationGroupItem(props: {
|
|||
onClick={() => setExpanded(!expanded)}
|
||||
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||
>
|
||||
{sourceContractTitle || contract ? (
|
||||
{sourceContractTitle ? (
|
||||
<span>
|
||||
{'Activity on '}
|
||||
<a
|
||||
href={
|
||||
sourceContractCreatorUsername
|
||||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
||||
: ''
|
||||
}
|
||||
className={
|
||||
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
}
|
||||
>
|
||||
{sourceContractTitle || contract?.question}
|
||||
{sourceContractTitle}
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
|
@ -306,6 +280,7 @@ function NotificationGroupItem(props: {
|
|||
)
|
||||
}
|
||||
|
||||
// TODO: where should we put referral bonus notifications?
|
||||
function NotificationSettings() {
|
||||
const user = useUser()
|
||||
const [notificationSettings, setNotificationSettings] =
|
||||
|
@ -455,6 +430,10 @@ function NotificationSettings() {
|
|||
highlight={notificationSettings !== 'none'}
|
||||
label={"Activity on questions you're betting on"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
highlight={notificationSettings !== 'none'}
|
||||
label={"Referral bonuses you've received"}
|
||||
/>
|
||||
<NotificationSettingLine
|
||||
label={"Activity on questions you've ever bet or commented on"}
|
||||
highlight={notificationSettings === 'all'}
|
||||
|
@ -515,7 +494,6 @@ function NotificationItem(props: {
|
|||
const { notification, justSummary } = props
|
||||
const {
|
||||
sourceType,
|
||||
sourceContractId,
|
||||
sourceId,
|
||||
sourceUserName,
|
||||
sourceUserAvatarUrl,
|
||||
|
@ -534,60 +512,15 @@ function NotificationItem(props: {
|
|||
|
||||
const [defaultNotificationText, setDefaultNotificationText] =
|
||||
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(() => {
|
||||
if (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) {
|
||||
// Handle arbitrary notifications with reason text here.
|
||||
setDefaultNotificationText(reasonText)
|
||||
}
|
||||
}, [
|
||||
contract,
|
||||
reasonText,
|
||||
sourceContractId,
|
||||
sourceId,
|
||||
sourceText,
|
||||
sourceType,
|
||||
sourceUpdateType,
|
||||
])
|
||||
}, [reasonText, sourceText])
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsAsSeen([notification])
|
||||
|
@ -596,14 +529,16 @@ function NotificationItem(props: {
|
|||
function getSourceUrl() {
|
||||
if (sourceType === 'follow') return `/${sourceUserUsername}`
|
||||
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
|
||||
if (
|
||||
sourceContractCreatorUsername &&
|
||||
sourceContractSlug &&
|
||||
sourceType === 'user'
|
||||
)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
if (sourceContractCreatorUsername && sourceContractSlug)
|
||||
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
||||
sourceId ?? ''
|
||||
)}`
|
||||
if (!contract) return ''
|
||||
return `/${contract.creatorUsername}/${
|
||||
contract.slug
|
||||
}#${getSourceIdForLinkComponent(sourceId ?? '')}`
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||
|
@ -669,13 +572,13 @@ function NotificationItem(props: {
|
|||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
contract,
|
||||
undefined,
|
||||
true
|
||||
).replace(' on', '')}
|
||||
</span>
|
||||
<div className={'ml-1 text-black'}>
|
||||
<NotificationTextLabel
|
||||
contract={contract}
|
||||
contract={null}
|
||||
defaultText={defaultNotificationText}
|
||||
className={'line-clamp-1'}
|
||||
notification={notification}
|
||||
|
@ -717,7 +620,9 @@ function NotificationItem(props: {
|
|||
sourceType,
|
||||
reason,
|
||||
sourceUpdateType,
|
||||
contract
|
||||
undefined,
|
||||
false,
|
||||
sourceSlug
|
||||
)}
|
||||
<a
|
||||
href={
|
||||
|
@ -725,13 +630,13 @@ function NotificationItem(props: {
|
|||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
: sourceType === 'group' && sourceSlug
|
||||
? `${groupPath(sourceSlug)}`
|
||||
: `/${contract?.creatorUsername}/${contract?.slug}`
|
||||
: ''
|
||||
}
|
||||
className={
|
||||
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
}
|
||||
>
|
||||
{contract?.question || sourceContractTitle || sourceTitle}
|
||||
{sourceContractTitle || sourceTitle}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
@ -752,7 +657,7 @@ function NotificationItem(props: {
|
|||
</Row>
|
||||
<div className={'mt-1 ml-1 md:text-base'}>
|
||||
<NotificationTextLabel
|
||||
contract={contract}
|
||||
contract={null}
|
||||
defaultText={defaultNotificationText}
|
||||
notification={notification}
|
||||
/>
|
||||
|
@ -811,6 +716,16 @@ function NotificationTextLabel(props: {
|
|||
</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) {
|
||||
return (
|
||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||
|
@ -829,7 +744,8 @@ function getReasonForShowingNotification(
|
|||
reason: notification_reason_types,
|
||||
sourceUpdateType: notification_source_update_types | undefined,
|
||||
contract: Contract | undefined | null,
|
||||
simple?: boolean
|
||||
simple?: boolean,
|
||||
sourceSlug?: string
|
||||
) {
|
||||
let reasonText: string
|
||||
switch (source) {
|
||||
|
@ -883,6 +799,12 @@ function getReasonForShowingNotification(
|
|||
case 'group':
|
||||
reasonText = 'added you to the group'
|
||||
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:
|
||||
reasonText = ''
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user