Add referral bonuses and notifications for them

This commit is contained in:
Ian Philips 2022-06-28 18:36:32 -05:00
parent 60e41a7f79
commit 359fef7ab4
16 changed files with 397 additions and 172 deletions

View File

@ -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'

View File

@ -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

View File

@ -33,6 +33,9 @@ export type User = {
followerCountCached: number
followedCategories?: string[]
referredByUserId?: string
referredByContractId?: string
}
export const STARTING_BALANCE = 1000

View File

@ -16,7 +16,7 @@ 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', 'referredByUserId']);
}
match /{somePath=**}/portfolioHistory/{portfolioHistoryId} {

View File

@ -5,6 +5,15 @@ import { User } from 'common/user'
import { Manalink } from 'common/manalink'
import { runTxn, TxnData } from './transact'
// if the userId claiming the manalink just signed up:
// - the manalink funds come from the house
// -
// if the manalink has a refergroupid:
// - add the user to the group
// in on-create-txn detect if it was a referral bonus, create notification for both parties
// necessary for manalink: authentication from fromUser
// - sending user's funds
// - allowing new user to join closed group
export const claimManalink = functions
.runWith({ minInstances: 1 })
.https.onCall(async (slug: string, context) => {

View File

@ -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
}

View File

@ -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'

View File

@ -1,18 +1,102 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { User } from '../../common/user'
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes'
import { getValues, getContract } from './utils'
import { createNotification } from './create-notification'
import { removeUndefinedProps } from '../../common/util/object'
import { ReferralTxn, Txn } from '../../common/txn'
import { Contract } from '../../common/contract'
const firestore = admin.firestore()
export const onUpdateGroup = functions.firestore
export const ReferredUserDescriptionPrefix = 'Referred user id'
export const onUpdateUser = functions.firestore
.document('users/{userId}')
.onUpdate(async (change) => {
.onUpdate(async (change, context) => {
const prevUser = change.before.data() as User
const user = change.after.data() as User
const { eventId } = context
// if they're updating their referredId, send the
if (prevUser.referredByUserId === user.referredByUserId) {
console.log("referredByUserId hasn't changed")
return // We only handle referrals right now.
}
await firestore
.collection('groups')
.doc(group.id)
.update({ mostRecentActivityTime: Date.now() })
// 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
// get user that referred this user
const referredByUserDoc = firestore.doc(`users/${referredByUserId}`)
const referredByUserSnap = await referredByUserDoc.get()
if (!referredByUserSnap.exists) {
console.log(`User ${referredByUserId} not found`)
return
}
const referredByUser = referredByUserSnap.data() as User
let referredByContract: Contract | undefined = undefined
if (user.referredByContractId)
referredByContract = await getContract(user.referredByContractId)
console.log(`referredByContract: ${referredByContract}`)
const txnQuery = firestore
.collection('txns')
.where('toId', '==', referredByUserId)
.where('category', '==', 'REFERRAL')
// find txns to this user, find if they have a txn that contains a BANK category txn that contains 'referred by' the current username in the description
const referralTxns = await getValues<Txn>(txnQuery).catch((err) => {
console.error('error getting txns:', err)
return []
})
if (referralTxns.map((txn) => txn.description).includes(referredByUserId)) {
console.log('found referral txn with the same details, aborting')
return
}
console.log('creating referral txns')
// TODO: change this to prod id
const fromId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' //HOUSE_LIQUIDITY_PROVIDER_ID
const referralAmount = 500
await firestore.runTransaction(async (transaction) => {
// 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: referralAmount,
token: 'M$',
category: 'REFERRAL',
description: `${ReferredUserDescriptionPrefix}: ${user.id} for ${referralAmount}`,
}
const txnDoc = await firestore.collection(`txns/`).doc(txn.id)
await transaction.set(txnDoc, removeUndefinedProps(txn))
console.log('created referral with txn id:', txn.id)
transaction.update(referredByUserDoc, {
balance: referredByUser.balance + referralAmount,
totalDeposits: referredByUser.totalDeposits + referralAmount,
})
await createNotification(
user.id,
'user',
'updated',
user,
eventId,
txn.amount.toString(),
referredByContract,
'user',
referredByUser.id,
referredByContract?.slug,
referredByContract?.question
)
})
})

View File

@ -29,6 +29,9 @@ 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 { CopyLinkButton } from 'web/components/copy-link-button'
import { ShareIconButton } from 'web/components/share-icon-button'
import { useUser } from 'web/hooks/use-user'
export type ShowTime = 'resolve-date' | 'close-date'
@ -130,6 +133,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 +196,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>

View File

@ -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)}

View File

@ -0,0 +1,49 @@
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'
function copyContractWithReferral(contract: Contract, username?: string) {
const postFix =
username && contract.creatorUsername !== username
? '?referrer=' + username
: ''
copyToClipboard(
`https://${ENV_CONFIG.domain}${contractPath(contract)}${postFix}`
)
}
export function ShareIconButton(props: {
contract: Contract
buttonClassName?: string
toastClassName?: string
username?: string
}) {
const { contract, buttonClassName, toastClassName, username } = props
const [showToast, setShowToast] = useState(false)
return (
<div className="relative z-10 flex-shrink-0">
<button
className={clsx(ContractDetailsButtonClassName, buttonClassName)}
onClick={() => {
copyContractWithReferral(contract, username)
track('copy share link')
setShowToast(true)
setTimeout(() => setShowToast(false), 2000)
}}
>
<ShareIcon className="h-[24px] w-5" aria-hidden="true" />
</button>
{showToast && <ToastClipboard className={toastClassName} />}
</div>
)
}

View File

@ -94,3 +94,13 @@ export async function getGroupsWithContractId(
const groups = await getValues<Group>(q)
setGroups(groups)
}
export async function addUserToGroupViaSlug(groupSlug: string, userId: string) {
// get group to get the member ids
const group = await getGroupBySlug(groupSlug)
if (group && !group.memberIds.includes(userId))
return await updateGroup(group, {
memberIds: [userId, ...group.memberIds],
})
return null
}

View File

@ -32,6 +32,9 @@ 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'
export type { User }
@ -88,12 +91,83 @@ 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(
contractUsername: 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 || contractUsername
)
// 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,
})
)
.then((data) => console.log('done!', data))
.catch(console.error)
})
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)
@ -117,6 +191,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)
@ -126,6 +201,21 @@ export function listenForLogin(onUser: (user: User | null) => void) {
})
}
// create new signup banner on markets for referrees
// markets that someone signs up to bet on are referred to the market's creator
// it would be great if you could invite someone to your group/allowlist their email
// invite a user to your group with a unique link
// pass the referrer id to this function
// add the referrer id to the new user's field
// in on-update-user check if the new user had a referrer,add the new user's ide to the referrees field
// create a txn for both
// create on-create-txn that checks why it was created, create a notification for it
// manalinks no required for:
// referral from: user to site, user to another user's market, open group, implicit from market creator
// from groups or market page, get the manalink id from the url and pass it here
// then claim it upon successful signup
//
export async function firebaseLogin() {
const provider = new GoogleAuthProvider()
return signInWithPopup(auth, provider)

View File

@ -10,7 +10,13 @@ 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 {
getUserByUsername,
listUsers,
updateUser,
User,
writeReferralInfo,
} from 'web/lib/firebase/users'
import {
Contract,
getContractFromSlug,
@ -42,6 +48,9 @@ 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 dayjs from 'dayjs'
import { addUserToGroupViaSlug, getGroup } from 'web/lib/firebase/groups'
export const getStaticProps = fromPropz(getStaticPropz)
export async function getStaticPropz(props: {
@ -147,6 +156,14 @@ export function ContractPageContent(
const ogCardProps = getOpenGraphProps(contract)
const router = useRouter()
const { referrer, username } = router.query as {
username: string
referrer?: string
}
if (!user && router.isReady)
writeReferralInfo(username, contract.id, referrer)
const rightSidebar = hasSidePanel ? (
<Col className="gap-4">
{allowTrade &&

View File

@ -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'
@ -146,6 +151,12 @@ export default function GroupPage(props: {
const isCreator = user && group && user.id === group.creatorId
const isMember = user && memberIds.includes(user.id)
const { referrer } = router.query as {
referrer?: string
}
if (!user && router.isReady)
writeReferralInfo(creator.username, undefined, referrer, group.slug)
const rightSidebar = (
<Col className="mt-6 hidden xl:block">
<GroupOverview

View File

@ -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>
) : (
@ -515,7 +489,6 @@ function NotificationItem(props: {
const { notification, justSummary } = props
const {
sourceType,
sourceContractId,
sourceId,
sourceUserName,
sourceUserAvatarUrl,
@ -534,60 +507,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 +524,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 +549,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 +567,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 +615,9 @@ function NotificationItem(props: {
sourceType,
reason,
sourceUpdateType,
contract
undefined,
false,
sourceSlug
)}
<a
href={
@ -725,13 +625,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 +652,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 +711,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 +739,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 +794,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 = ''
}