diff --git a/common/notification.ts b/common/notification.ts index d6615514..845a859e 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -18,6 +18,7 @@ export type Notification = { sourceContractTitle?: string sourceContractCreatorUsername?: string sourceContractSlug?: string + sourceContractTags?: string[] } export type notification_source_types = | 'contract' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 892b42eb..83ba9f24 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -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 } diff --git a/functions/src/index.ts b/functions/src/index.ts index ecc17133..88a13759 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -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' \ No newline at end of file +export * from './withdraw-liquidity' diff --git a/functions/src/market-close-emails.ts b/functions/src/market-close-notifications.ts similarity index 81% rename from functions/src/market-close-emails.ts rename to functions/src/market-close-notifications.ts index 17700613..ee9952bf 100644 --- a/functions/src/market-close-emails.ts +++ b/functions/src/market-close-notifications.ts @@ -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 + ) } } diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts index 040ba378..20c7ceba 100644 --- a/functions/src/on-create-contract.ts +++ b/functions/src/on-create-contract.ts @@ -18,7 +18,7 @@ export const onCreateContract = functions.firestore 'created', contractCreator, eventId, - contract.question, + contract.description, contract ) }) diff --git a/functions/src/on-create-liquidity-provision.ts b/functions/src/on-create-liquidity-provision.ts new file mode 100644 index 00000000..189b4693 --- /dev/null +++ b/functions/src/on-create-liquidity-provision.ts @@ -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 + ) + }) diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index 8b16b3d6..f47c019c 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -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 ) } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index e4a118e4..8130ed22 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -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 - - if ( - isNotificationAboutContractResolution( - sourceType, - sourceUpdateType, - contract - ) - ) { - if (sourceText) { - if (sourceText === 'YES' || sourceText == 'NO') { - return - } - if (sourceText.includes('%')) - return ( - - ) - if (sourceText === 'CANCEL') return - if (sourceText === 'MKT' || sourceText === 'PROB') return - // Show free response answer text - return {sourceText} - } else if (contract?.resolution) { - if (contract.outcomeType === 'FREE_RESPONSE') { - return ( - - ) - } - return ( - - ) - } else return
- } else if (sourceType === 'contract') { + if (sourceType === 'contract') { if (justSummary) return {contract?.question || sourceContractTitle} - // Ignore contract update source text until we improve them - return
- } else { + if (!sourceText) return
+ // Resolved contracts + if ( + isNotificationAboutContractResolution( + sourceType, + sourceUpdateType, + contract + ) + ) { + { + if (sourceText === 'YES' || sourceText == 'NO') { + return + } + if (sourceText.includes('%')) + return ( + + ) + if (sourceText === 'CANCEL') return + if (sourceText === 'MKT' || sourceText === 'PROB') return + } + } + // Close date will be a number - it looks better without it + if (sourceUpdateType === 'closed') { + return
+ } + // Updated contracts + // Description will be in default text + if (parseInt(sourceText) > 0) { + return ( + + Updated close time: {new Date(parseInt(sourceText)).toLocaleString()} + + ) + } + } else if (sourceType === 'liquidity' && sourceText) { return ( -
- -
+ {formatMoney(parseInt(sourceText))} ) } + // return default text + return ( +
+ +
+ ) } 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 = '' }