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:
Ian Philips 2022-06-10 16:48:28 -06:00 committed by GitHub
parent 8bdc33f683
commit 89784bf5eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 85 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ export const onCreateContract = functions.firestore
'created', 'created',
contractCreator, contractCreator,
eventId, eventId,
contract.question, contract.description,
contract contract
) )
}) })

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

View File

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

View File

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