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
sourceContractCreatorUsername?: string
sourceContractSlug?: string
sourceContractTags?: string[]
}
export type notification_source_types =
| 'contract'

View File

@ -66,12 +66,31 @@ export const createNotification = async (
sourceContractTitle: sourceContract?.question,
sourceContractCreatorUsername: sourceContract?.creatorUsername,
sourceContractSlug: sourceContract?.slug,
sourceContractTags: sourceContract?.tags,
}
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 (
userToReasonTexts: user_to_reason_texts
) => {
@ -94,14 +113,11 @@ export const createNotification = async (
}
const notifyRepliedUsers = async (
userToReasonTexts: user_to_reason_texts
userToReasonTexts: user_to_reason_texts,
relatedUserId: string,
relatedSourceType: notification_source_types
) => {
if (
!relatedSourceType ||
!relatedUserId ||
!shouldGetNotification(relatedUserId, userToReasonTexts)
)
return
if (!shouldGetNotification(relatedUserId, userToReasonTexts)) return
if (relatedSourceType === 'comment') {
userToReasonTexts[relatedUserId] = {
reason: 'reply_to_users_comment',
@ -123,8 +139,10 @@ export const createNotification = async (
}
}
const notifyTaggedUsers = async (userToReasonTexts: user_to_reason_texts) => {
if (!sourceText) return
const notifyTaggedUsers = async (
userToReasonTexts: user_to_reason_texts,
sourceText: string
) => {
const taggedUsers = sourceText.match(/@\w+/g)
if (!taggedUsers) return
// await all get tagged users:
@ -143,9 +161,13 @@ export const createNotification = async (
const notifyContractCreator = async (
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] = {
reason: 'on_users_contract',
}
@ -189,7 +211,7 @@ export const createNotification = async (
})
}
const notifyOtherBettorsOnContract = async (
const notifyBettorsOnContract = async (
userToReasonTexts: user_to_reason_texts,
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 userToReasonTexts: user_to_reason_texts = {}
// The following functions modify the userToReasonTexts object in place.
if (
sourceContract &&
(sourceType === 'comment' ||
if (sourceContract) {
if (
sourceType === 'comment' ||
sourceType === 'answer' ||
(sourceType === 'contract' &&
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')))
) {
if (sourceType === 'comment') {
await notifyRepliedUsers(userToReasonTexts)
await notifyTaggedUsers(userToReasonTexts)
(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)
}
await notifyContractCreator(userToReasonTexts, sourceContract)
await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract)
await notifyOtherBettorsOnContract(userToReasonTexts, sourceContract)
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
} else if (sourceType === 'follow' && relatedUserId) {
await notifyFollowedUser(userToReasonTexts, relatedUserId)
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
await notifyUsersFollowers(userToReasonTexts)
}
return userToReasonTexts
}

View File

@ -22,12 +22,13 @@ export * from './update-recommendations'
export * from './update-feed'
export * from './backup-db'
export * from './change-user-info'
export * from './market-close-emails'
export * from './market-close-notifications'
export * from './add-liquidity'
export * from './on-create-answer'
export * from './on-update-contract'
export * from './on-create-contract'
export * from './on-follow-user'
export * from './on-create-liquidity-provision'
// v2
export * from './health'
@ -35,4 +36,4 @@ export * from './place-bet'
export * from './sell-bet'
export * from './sell-shares'
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 { getPrivateUser, getUserByUsername } from './utils'
import { sendMarketCloseEmail } from './emails'
import { createNotification } from './create-notification'
export const marketCloseEmails = functions
export const marketCloseNotifications = functions
.runWith({ secrets: ['MAILGUN_KEY'] })
.pubsub.schedule('every 1 hours')
.onRun(async () => {
@ -56,5 +57,14 @@ async function sendMarketCloseEmails() {
if (!privateUser) continue
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',
contractCreator,
eventId,
contract.question,
contract.description,
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.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(
contract.id,
'contract',
'updated',
contractUpdater,
eventId,
contract.question,
sourceText,
contract
)
}

View File

@ -44,6 +44,7 @@ import {
import { getContractFromId } from 'web/lib/firebase/contracts'
import { CheckIcon, XIcon } from '@heroicons/react/outline'
import toast from 'react-hot-toast'
import { formatMoney, formatPercent } from 'common/util/format'
export default function Notifications() {
const user = useUser()
@ -565,7 +566,7 @@ function NotificationItem(props: {
sourceType === 'contract'
) {
try {
parseNotificationText(
parseOldStyleNotificationText(
sourceId,
sourceContractId,
sourceType,
@ -619,7 +620,7 @@ function NotificationItem(props: {
}
}
async function parseNotificationText(
async function parseOldStyleNotificationText(
sourceId: string,
sourceContractId: string,
sourceType: 'answer' | 'comment' | 'contract',
@ -772,61 +773,54 @@ function NotificationTextLabel(props: {
const { contract, className, defaultText, notification, justSummary } = props
const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } =
notification
if (!contract && !sourceText && sourceType !== 'follow')
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 (sourceType === 'contract') {
if (justSummary)
return <span>{contract?.question || sourceContractTitle}</span>
// Ignore contract update source text until we improve them
return <div />
} else {
if (!sourceText) return <div />
// Resolved contracts
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 (
<div
className={className ? className : 'line-clamp-4 whitespace-pre-line'}
>
<Linkify text={defaultText} />
</div>
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
)
}
// return default text
return (
<div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
<Linkify text={defaultText} />
</div>
)
}
function getReasonForShowingNotification(
@ -865,6 +859,8 @@ function getReasonForShowingNotification(
)
)
reasonText = `resolved`
else if (sourceUpdateType === 'closed')
reasonText = `please resolve your question`
else reasonText = `updated`
break
case 'answer':
@ -880,6 +876,9 @@ function getReasonForShowingNotification(
case 'follow':
reasonText = 'followed you'
break
case 'liquidity':
reasonText = 'added liquidity to your question'
break
default:
reasonText = ''
}