Notifications for liquidity proiders/provisions (#478)
* Notifications for liquidity proiders/provisions * typo * Rename * Return default text * Marke needs resolution notifications * remove todo
This commit is contained in:
parent
8bdc33f683
commit
89784bf5eb
|
@ -18,6 +18,7 @@ export type Notification = {
|
||||||
sourceContractTitle?: string
|
sourceContractTitle?: string
|
||||||
sourceContractCreatorUsername?: string
|
sourceContractCreatorUsername?: string
|
||||||
sourceContractSlug?: string
|
sourceContractSlug?: string
|
||||||
|
sourceContractTags?: string[]
|
||||||
}
|
}
|
||||||
export type notification_source_types =
|
export type notification_source_types =
|
||||||
| 'contract'
|
| 'contract'
|
||||||
|
|
|
@ -66,12 +66,31 @@ export const createNotification = async (
|
||||||
sourceContractTitle: sourceContract?.question,
|
sourceContractTitle: sourceContract?.question,
|
||||||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||||
sourceContractSlug: sourceContract?.slug,
|
sourceContractSlug: sourceContract?.slug,
|
||||||
|
sourceContractTags: sourceContract?.tags,
|
||||||
}
|
}
|
||||||
await notificationRef.set(removeUndefinedProps(notification))
|
await notificationRef.set(removeUndefinedProps(notification))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notifyLiquidityProviders = async (
|
||||||
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
contract: Contract
|
||||||
|
) => {
|
||||||
|
const liquidityProviders = await firestore
|
||||||
|
.collection(`contracts/${contract.id}/liquidity`)
|
||||||
|
.get()
|
||||||
|
const liquidityProvidersIds = uniq(
|
||||||
|
liquidityProviders.docs.map((doc) => doc.data().userId)
|
||||||
|
)
|
||||||
|
liquidityProvidersIds.forEach((userId) => {
|
||||||
|
if (!shouldGetNotification(userId, userToReasonTexts)) return
|
||||||
|
userToReasonTexts[userId] = {
|
||||||
|
reason: 'on_contract_with_users_shares_in',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const notifyUsersFollowers = async (
|
const notifyUsersFollowers = async (
|
||||||
userToReasonTexts: user_to_reason_texts
|
userToReasonTexts: user_to_reason_texts
|
||||||
) => {
|
) => {
|
||||||
|
@ -94,14 +113,11 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyRepliedUsers = async (
|
const notifyRepliedUsers = async (
|
||||||
userToReasonTexts: user_to_reason_texts
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
relatedUserId: string,
|
||||||
|
relatedSourceType: notification_source_types
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (!shouldGetNotification(relatedUserId, userToReasonTexts)) return
|
||||||
!relatedSourceType ||
|
|
||||||
!relatedUserId ||
|
|
||||||
!shouldGetNotification(relatedUserId, userToReasonTexts)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (relatedSourceType === 'comment') {
|
if (relatedSourceType === 'comment') {
|
||||||
userToReasonTexts[relatedUserId] = {
|
userToReasonTexts[relatedUserId] = {
|
||||||
reason: 'reply_to_users_comment',
|
reason: 'reply_to_users_comment',
|
||||||
|
@ -123,8 +139,10 @@ export const createNotification = async (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyTaggedUsers = async (userToReasonTexts: user_to_reason_texts) => {
|
const notifyTaggedUsers = async (
|
||||||
if (!sourceText) return
|
userToReasonTexts: user_to_reason_texts,
|
||||||
|
sourceText: string
|
||||||
|
) => {
|
||||||
const taggedUsers = sourceText.match(/@\w+/g)
|
const taggedUsers = sourceText.match(/@\w+/g)
|
||||||
if (!taggedUsers) return
|
if (!taggedUsers) return
|
||||||
// await all get tagged users:
|
// await all get tagged users:
|
||||||
|
@ -143,9 +161,13 @@ export const createNotification = async (
|
||||||
|
|
||||||
const notifyContractCreator = async (
|
const notifyContractCreator = async (
|
||||||
userToReasonTexts: user_to_reason_texts,
|
userToReasonTexts: user_to_reason_texts,
|
||||||
sourceContract: Contract
|
sourceContract: Contract,
|
||||||
|
options?: { force: boolean }
|
||||||
) => {
|
) => {
|
||||||
if (shouldGetNotification(sourceContract.creatorId, userToReasonTexts))
|
if (
|
||||||
|
options?.force ||
|
||||||
|
shouldGetNotification(sourceContract.creatorId, userToReasonTexts)
|
||||||
|
)
|
||||||
userToReasonTexts[sourceContract.creatorId] = {
|
userToReasonTexts[sourceContract.creatorId] = {
|
||||||
reason: 'on_users_contract',
|
reason: 'on_users_contract',
|
||||||
}
|
}
|
||||||
|
@ -189,7 +211,7 @@ export const createNotification = async (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const notifyOtherBettorsOnContract = async (
|
const notifyBettorsOnContract = async (
|
||||||
userToReasonTexts: user_to_reason_texts,
|
userToReasonTexts: user_to_reason_texts,
|
||||||
sourceContract: Contract
|
sourceContract: Contract
|
||||||
) => {
|
) => {
|
||||||
|
@ -216,30 +238,41 @@ export const createNotification = async (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Update for liquidity.
|
|
||||||
// TODO: Notify users of their own closed but not resolved contracts.
|
|
||||||
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 (
|
if (sourceContract) {
|
||||||
sourceContract &&
|
if (
|
||||||
(sourceType === 'comment' ||
|
sourceType === 'comment' ||
|
||||||
sourceType === 'answer' ||
|
sourceType === 'answer' ||
|
||||||
(sourceType === 'contract' &&
|
(sourceType === 'contract' &&
|
||||||
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')))
|
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved'))
|
||||||
) {
|
) {
|
||||||
if (sourceType === 'comment') {
|
if (sourceType === 'comment') {
|
||||||
await notifyRepliedUsers(userToReasonTexts)
|
if (relatedUserId && relatedSourceType)
|
||||||
await notifyTaggedUsers(userToReasonTexts)
|
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)
|
||||||
}
|
}
|
||||||
await notifyContractCreator(userToReasonTexts, sourceContract)
|
|
||||||
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
|
|
||||||
await notifyOtherBettorsOnContract(userToReasonTexts, sourceContract)
|
|
||||||
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
|
|
||||||
} else if (sourceType === 'follow' && relatedUserId) {
|
} else if (sourceType === 'follow' && relatedUserId) {
|
||||||
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
await notifyFollowedUser(userToReasonTexts, relatedUserId)
|
||||||
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
|
|
||||||
await notifyUsersFollowers(userToReasonTexts)
|
|
||||||
}
|
}
|
||||||
return userToReasonTexts
|
return userToReasonTexts
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,13 @@ export * from './update-recommendations'
|
||||||
export * from './update-feed'
|
export * from './update-feed'
|
||||||
export * from './backup-db'
|
export * from './backup-db'
|
||||||
export * from './change-user-info'
|
export * from './change-user-info'
|
||||||
export * from './market-close-emails'
|
export * from './market-close-notifications'
|
||||||
export * from './add-liquidity'
|
export * from './add-liquidity'
|
||||||
export * from './on-create-answer'
|
export * from './on-create-answer'
|
||||||
export * from './on-update-contract'
|
export * from './on-update-contract'
|
||||||
export * from './on-create-contract'
|
export * from './on-create-contract'
|
||||||
export * from './on-follow-user'
|
export * from './on-follow-user'
|
||||||
|
export * from './on-create-liquidity-provision'
|
||||||
|
|
||||||
// v2
|
// v2
|
||||||
export * from './health'
|
export * from './health'
|
||||||
|
@ -35,4 +36,4 @@ export * from './place-bet'
|
||||||
export * from './sell-bet'
|
export * from './sell-bet'
|
||||||
export * from './sell-shares'
|
export * from './sell-shares'
|
||||||
export * from './create-contract'
|
export * from './create-contract'
|
||||||
export * from './withdraw-liquidity'
|
export * from './withdraw-liquidity'
|
||||||
|
|
|
@ -4,8 +4,9 @@ import * as admin from 'firebase-admin'
|
||||||
import { Contract } from '../../common/contract'
|
import { Contract } from '../../common/contract'
|
||||||
import { getPrivateUser, getUserByUsername } from './utils'
|
import { getPrivateUser, getUserByUsername } from './utils'
|
||||||
import { sendMarketCloseEmail } from './emails'
|
import { sendMarketCloseEmail } from './emails'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
|
||||||
export const marketCloseEmails = functions
|
export const marketCloseNotifications = functions
|
||||||
.runWith({ secrets: ['MAILGUN_KEY'] })
|
.runWith({ secrets: ['MAILGUN_KEY'] })
|
||||||
.pubsub.schedule('every 1 hours')
|
.pubsub.schedule('every 1 hours')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
|
@ -56,5 +57,14 @@ async function sendMarketCloseEmails() {
|
||||||
if (!privateUser) continue
|
if (!privateUser) continue
|
||||||
|
|
||||||
await sendMarketCloseEmail(user, privateUser, contract)
|
await sendMarketCloseEmail(user, privateUser, contract)
|
||||||
|
await createNotification(
|
||||||
|
contract.id,
|
||||||
|
'contract',
|
||||||
|
'closed',
|
||||||
|
user,
|
||||||
|
'closed' + contract.id.slice(6, contract.id.length),
|
||||||
|
contract.closeTime?.toString() ?? new Date().toString(),
|
||||||
|
contract
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@ export const onCreateContract = functions.firestore
|
||||||
'created',
|
'created',
|
||||||
contractCreator,
|
contractCreator,
|
||||||
eventId,
|
eventId,
|
||||||
contract.question,
|
contract.description,
|
||||||
contract
|
contract
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
28
functions/src/on-create-liquidity-provision.ts
Normal file
28
functions/src/on-create-liquidity-provision.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import * as functions from 'firebase-functions'
|
||||||
|
import { getContract, getUser } from './utils'
|
||||||
|
import { createNotification } from './create-notification'
|
||||||
|
import { LiquidityProvision } from 'common/liquidity-provision'
|
||||||
|
|
||||||
|
export const onCreateLiquidityProvision = functions.firestore
|
||||||
|
.document('contracts/{contractId}/liquidity/{liquidityId}')
|
||||||
|
.onCreate(async (change, context) => {
|
||||||
|
const liquidity = change.data() as LiquidityProvision
|
||||||
|
const { eventId } = context
|
||||||
|
const contract = await getContract(liquidity.contractId)
|
||||||
|
|
||||||
|
if (!contract)
|
||||||
|
throw new Error('Could not find contract corresponding with liquidity')
|
||||||
|
|
||||||
|
const liquidityProvider = await getUser(liquidity.userId)
|
||||||
|
if (!liquidityProvider) throw new Error('Could not find liquidity provider')
|
||||||
|
|
||||||
|
await createNotification(
|
||||||
|
contract.id,
|
||||||
|
'liquidity',
|
||||||
|
'created',
|
||||||
|
liquidityProvider,
|
||||||
|
eventId,
|
||||||
|
liquidity.amount.toString(),
|
||||||
|
contract
|
||||||
|
)
|
||||||
|
})
|
|
@ -39,13 +39,26 @@ export const onUpdateContract = functions.firestore
|
||||||
previousValue.closeTime !== contract.closeTime ||
|
previousValue.closeTime !== contract.closeTime ||
|
||||||
previousValue.description !== contract.description
|
previousValue.description !== contract.description
|
||||||
) {
|
) {
|
||||||
|
let sourceText = ''
|
||||||
|
if (previousValue.closeTime !== contract.closeTime && contract.closeTime)
|
||||||
|
sourceText = contract.closeTime.toString()
|
||||||
|
else {
|
||||||
|
const oldTrimmedDescription = previousValue.description.trim()
|
||||||
|
const newTrimmedDescription = contract.description.trim()
|
||||||
|
if (oldTrimmedDescription === '') sourceText = newTrimmedDescription
|
||||||
|
else
|
||||||
|
sourceText = newTrimmedDescription
|
||||||
|
.split(oldTrimmedDescription)[1]
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
await createNotification(
|
await createNotification(
|
||||||
contract.id,
|
contract.id,
|
||||||
'contract',
|
'contract',
|
||||||
'updated',
|
'updated',
|
||||||
contractUpdater,
|
contractUpdater,
|
||||||
eventId,
|
eventId,
|
||||||
contract.question,
|
sourceText,
|
||||||
contract
|
contract
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ import {
|
||||||
import { getContractFromId } from 'web/lib/firebase/contracts'
|
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, formatPercent } from 'common/util/format'
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -565,7 +566,7 @@ function NotificationItem(props: {
|
||||||
sourceType === 'contract'
|
sourceType === 'contract'
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
parseNotificationText(
|
parseOldStyleNotificationText(
|
||||||
sourceId,
|
sourceId,
|
||||||
sourceContractId,
|
sourceContractId,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
@ -619,7 +620,7 @@ function NotificationItem(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseNotificationText(
|
async function parseOldStyleNotificationText(
|
||||||
sourceId: string,
|
sourceId: string,
|
||||||
sourceContractId: string,
|
sourceContractId: string,
|
||||||
sourceType: 'answer' | 'comment' | 'contract',
|
sourceType: 'answer' | 'comment' | 'contract',
|
||||||
|
@ -772,61 +773,54 @@ function NotificationTextLabel(props: {
|
||||||
const { contract, className, defaultText, notification, justSummary } = props
|
const { contract, className, defaultText, notification, justSummary } = props
|
||||||
const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } =
|
const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } =
|
||||||
notification
|
notification
|
||||||
if (!contract && !sourceText && sourceType !== 'follow')
|
if (sourceType === 'contract') {
|
||||||
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
|
|
||||||
|
|
||||||
if (
|
|
||||||
isNotificationAboutContractResolution(
|
|
||||||
sourceType,
|
|
||||||
sourceUpdateType,
|
|
||||||
contract
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
if (sourceText) {
|
|
||||||
if (sourceText === 'YES' || sourceText == 'NO') {
|
|
||||||
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
|
||||||
}
|
|
||||||
if (sourceText.includes('%'))
|
|
||||||
return (
|
|
||||||
<ProbPercentLabel prob={parseFloat(sourceText.replace('%', ''))} />
|
|
||||||
)
|
|
||||||
if (sourceText === 'CANCEL') return <CancelLabel />
|
|
||||||
if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel />
|
|
||||||
// Show free response answer text
|
|
||||||
return <span>{sourceText}</span>
|
|
||||||
} else if (contract?.resolution) {
|
|
||||||
if (contract.outcomeType === 'FREE_RESPONSE') {
|
|
||||||
return (
|
|
||||||
<FreeResponseOutcomeLabel
|
|
||||||
contract={contract}
|
|
||||||
resolution={contract.resolution}
|
|
||||||
truncate={'long'}
|
|
||||||
answerClassName={className}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<OutcomeLabel
|
|
||||||
contract={contract}
|
|
||||||
outcome={contract.resolution}
|
|
||||||
truncate={'long'}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
} else return <div />
|
|
||||||
} else if (sourceType === 'contract') {
|
|
||||||
if (justSummary)
|
if (justSummary)
|
||||||
return <span>{contract?.question || sourceContractTitle}</span>
|
return <span>{contract?.question || sourceContractTitle}</span>
|
||||||
// Ignore contract update source text until we improve them
|
if (!sourceText) return <div />
|
||||||
return <div />
|
// Resolved contracts
|
||||||
} else {
|
if (
|
||||||
|
isNotificationAboutContractResolution(
|
||||||
|
sourceType,
|
||||||
|
sourceUpdateType,
|
||||||
|
contract
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
{
|
||||||
|
if (sourceText === 'YES' || sourceText == 'NO') {
|
||||||
|
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
||||||
|
}
|
||||||
|
if (sourceText.includes('%'))
|
||||||
|
return (
|
||||||
|
<ProbPercentLabel prob={parseFloat(sourceText.replace('%', ''))} />
|
||||||
|
)
|
||||||
|
if (sourceText === 'CANCEL') return <CancelLabel />
|
||||||
|
if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Close date will be a number - it looks better without it
|
||||||
|
if (sourceUpdateType === 'closed') {
|
||||||
|
return <div />
|
||||||
|
}
|
||||||
|
// Updated contracts
|
||||||
|
// Description will be in default text
|
||||||
|
if (parseInt(sourceText) > 0) {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Updated close time: {new Date(parseInt(sourceText)).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (sourceType === 'liquidity' && sourceText) {
|
||||||
return (
|
return (
|
||||||
<div
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||||
className={className ? className : 'line-clamp-4 whitespace-pre-line'}
|
|
||||||
>
|
|
||||||
<Linkify text={defaultText} />
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// return default text
|
||||||
|
return (
|
||||||
|
<div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
|
||||||
|
<Linkify text={defaultText} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getReasonForShowingNotification(
|
function getReasonForShowingNotification(
|
||||||
|
@ -865,6 +859,8 @@ function getReasonForShowingNotification(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
reasonText = `resolved`
|
reasonText = `resolved`
|
||||||
|
else if (sourceUpdateType === 'closed')
|
||||||
|
reasonText = `please resolve your question`
|
||||||
else reasonText = `updated`
|
else reasonText = `updated`
|
||||||
break
|
break
|
||||||
case 'answer':
|
case 'answer':
|
||||||
|
@ -880,6 +876,9 @@ function getReasonForShowingNotification(
|
||||||
case 'follow':
|
case 'follow':
|
||||||
reasonText = 'followed you'
|
reasonText = 'followed you'
|
||||||
break
|
break
|
||||||
|
case 'liquidity':
|
||||||
|
reasonText = 'added liquidity to your question'
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
reasonText = ''
|
reasonText = ''
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user