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 (sourceContract) {
if ( if (
sourceContract && 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 notifyContractCreator(userToReasonTexts, sourceContract)
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
await notifyOtherBettorsOnContract(userToReasonTexts, sourceContract) await notifyLiquidityProviders(userToReasonTexts, sourceContract)
await notifyBettorsOnContract(userToReasonTexts, sourceContract)
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
} else if (sourceType === 'follow' && relatedUserId) {
await notifyFollowedUser(userToReasonTexts, relatedUserId)
} else if (sourceType === 'contract' && sourceUpdateType === 'created') { } else if (sourceType === 'contract' && sourceUpdateType === 'created') {
await notifyUsersFollowers(userToReasonTexts) await notifyUsersFollowers(userToReasonTexts)
} else if (sourceType === 'contract' && sourceUpdateType === 'closed') {
await notifyContractCreator(userToReasonTexts, sourceContract, {
force: true,
})
} else if (sourceType === 'liquidity' && sourceUpdateType === 'created') {
await notifyContractCreator(userToReasonTexts, sourceContract)
}
} else if (sourceType === 'follow' && relatedUserId) {
await notifyFollowedUser(userToReasonTexts, relatedUserId)
} }
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'

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,9 +773,11 @@ 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 (justSummary)
return <span>{contract?.question || sourceContractTitle}</span>
if (!sourceText) return <div />
// Resolved contracts
if ( if (
isNotificationAboutContractResolution( isNotificationAboutContractResolution(
sourceType, sourceType,
@ -782,7 +785,7 @@ function NotificationTextLabel(props: {
contract contract
) )
) { ) {
if (sourceText) { {
if (sourceText === 'YES' || sourceText == 'NO') { if (sourceText === 'YES' || sourceText == 'NO') {
return <BinaryOutcomeLabel outcome={sourceText as any} /> return <BinaryOutcomeLabel outcome={sourceText as any} />
} }
@ -792,42 +795,33 @@ function NotificationTextLabel(props: {
) )
if (sourceText === 'CANCEL') return <CancelLabel /> if (sourceText === 'CANCEL') return <CancelLabel />
if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel /> if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel />
// Show free response answer text }
return <span>{sourceText}</span> }
} else if (contract?.resolution) { // Close date will be a number - it looks better without it
if (contract.outcomeType === 'FREE_RESPONSE') { if (sourceUpdateType === 'closed') {
return <div />
}
// Updated contracts
// Description will be in default text
if (parseInt(sourceText) > 0) {
return ( return (
<FreeResponseOutcomeLabel <span>
contract={contract} Updated close time: {new Date(parseInt(sourceText)).toLocaleString()}
resolution={contract.resolution} </span>
truncate={'long'}
answerClassName={className}
/>
) )
} }
} else if (sourceType === 'liquidity' && sourceText) {
return ( return (
<OutcomeLabel <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
contract={contract}
outcome={contract.resolution}
truncate={'long'}
/>
) )
} else return <div /> }
} else if (sourceType === 'contract') { // return default text
if (justSummary)
return <span>{contract?.question || sourceContractTitle}</span>
// Ignore contract update source text until we improve them
return <div />
} else {
return ( return (
<div <div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
className={className ? className : 'line-clamp-4 whitespace-pre-line'}
>
<Linkify text={defaultText} /> <Linkify text={defaultText} />
</div> </div>
) )
} }
}
function getReasonForShowingNotification( function getReasonForShowingNotification(
source: notification_source_types, source: notification_source_types,
@ -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 = ''
} }