diff --git a/common/notification.ts b/common/notification.ts index 81c3b2fd..e9ccd594 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -1,4 +1,5 @@ -import { notification_subscription_types } from 'common/user' +import { notification_subscription_types, PrivateUser } from './user' +import { DOMAIN } from './envs/constants' export type Notification = { id: string @@ -53,6 +54,7 @@ export type notification_source_update_types = | 'deleted' | 'closed' +/* Optional - if possible use a keyof notification_subscription_types */ export type notification_reason_types = | 'tagged_user' | 'on_new_follow' @@ -90,7 +92,11 @@ export type notification_reason_types = | 'your_contract_closed' | 'subsidized_your_market' -// Adding a new key:value here is optional, you can also just use a key of notification_subscription_types +// Adding a new key:value here is optional, you can just use a key of notification_subscription_types +// You might want to add a key:value here if there will be multiple notification reasons that map to the same +// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to +// 'all_comments_on_watched_markets' subscription type +// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types export const notificationReasonToSubscriptionType: Partial< Record > = { @@ -127,3 +133,27 @@ export const notificationReasonToSubscriptionType: Partial< reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets', reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets', } + +export const getDestinationsForUser = async ( + privateUser: PrivateUser, + reason: notification_reason_types | keyof notification_subscription_types +) => { + const notificationSettings = privateUser.notificationSubscriptionTypes + let destinations + let subscriptionType: keyof notification_subscription_types | undefined + if (Object.keys(notificationSettings).includes(reason)) { + subscriptionType = reason as keyof notification_subscription_types + destinations = notificationSettings[subscriptionType] + } else { + const key = reason as notification_reason_types + subscriptionType = notificationReasonToSubscriptionType[key] + destinations = subscriptionType + ? notificationSettings[subscriptionType] + : [] + } + return { + sendToEmail: destinations.includes('email'), + sendToBrowser: destinations.includes('browser'), + urlToManageThisNotification: `${DOMAIN}/notifications?section=${subscriptionType}`, + } +} diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 8c14cd8b..4ac66a3f 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -1,10 +1,10 @@ import * as admin from 'firebase-admin' import { + getDestinationsForUser, Notification, notification_reason_types, - notificationReasonToSubscriptionType, } from '../../common/notification' -import { notification_subscription_types, User } from '../../common/user' +import { User } from '../../common/user' import { Contract } from '../../common/contract' import { getPrivateUser, getValues } from './utils' import { Comment } from '../../common/comment' @@ -23,7 +23,6 @@ import { sendNewAnswerEmail, sendNewCommentEmail, } from './emails' -import { DOMAIN } from 'common/lib/envs/constants' const firestore = admin.firestore() type recipients_to_reason_texts = { @@ -61,8 +60,12 @@ export const createNotification = async ( ) => { for (const userId in userToReasonTexts) { const { reason } = userToReasonTexts[userId] - const { sendToBrowser, sendToEmail, privateUser } = - await getDestinationsForUser(userId, reason) + const privateUser = await getPrivateUser(userId) + if (!privateUser) continue + const { sendToBrowser, sendToEmail } = await getDestinationsForUser( + privateUser, + reason + ) if (sendToBrowser) { const notificationRef = firestore .collection(`/users/${userId}/notifications`) @@ -93,7 +96,12 @@ export const createNotification = async ( if (!sendToEmail) continue if (reason === 'your_contract_closed' && privateUser && sourceContract) { - await sendMarketCloseEmail(reason, sourceUser, sourceContract) + await sendMarketCloseEmail( + reason, + sourceUser, + privateUser, + sourceContract + ) } else if (reason === 'tagged_user') { // TODO: send email to tagged user in new contract } else if (reason === 'subsidized_your_market') { @@ -197,35 +205,6 @@ export const createNotification = async ( await sendNotificationsIfSettingsPermit(userToReasonTexts) } -export const getDestinationsForUser = async ( - userId: string, - reason: notification_reason_types | keyof notification_subscription_types -) => { - const privateUser = await getPrivateUser(userId) - if (!privateUser) - return { sendToEmail: false, sendToBrowser: false, privateUser: null } - - const notificationSettings = privateUser.notificationSubscriptionTypes - let destinations - let subscriptionType: keyof notification_subscription_types | undefined - if (Object.keys(notificationSettings).includes(reason)) { - subscriptionType = reason as keyof notification_subscription_types - destinations = notificationSettings[subscriptionType] - } else { - const key = reason as notification_reason_types - subscriptionType = notificationReasonToSubscriptionType[key] - destinations = subscriptionType - ? notificationSettings[subscriptionType] - : [] - } - return { - sendToEmail: destinations.includes('email'), - sendToBrowser: destinations.includes('browser'), - privateUser, - urlToManageThisNotification: `${DOMAIN}/notifications?section=${subscriptionType}`, - } -} - export const createCommentOrAnswerOrUpdatedContractNotification = async ( sourceId: string, sourceType: 'comment' | 'answer' | 'contract', @@ -314,9 +293,10 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async ( recipientIdsList.includes(userId) ) return - + const privateUser = await getPrivateUser(userId) + if (!privateUser) return const { sendToBrowser, sendToEmail } = await getDestinationsForUser( - userId, + privateUser, reason ) @@ -328,7 +308,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async ( if (sourceType === 'comment') { await sendNewCommentEmail( reason, - userId, + privateUser, sourceUser, sourceContract, sourceText, @@ -341,7 +321,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async ( } else if (sourceType === 'answer') await sendNewAnswerEmail( reason, - userId, + privateUser, sourceUser.name, sourceText, sourceContract, @@ -354,7 +334,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async ( ) await sendMarketResolutionEmail( reason, - userId, + privateUser, resolutionData.userInvestments[userId], resolutionData.userPayouts[userId], sourceUser, @@ -534,8 +514,10 @@ export const createTipNotification = async ( contract?: Contract, group?: Group ) => { + const privateUser = await getPrivateUser(toUser.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - toUser.id, + privateUser, 'tip_received' ) if (!sendToBrowser) return @@ -565,6 +547,7 @@ export const createTipNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) + // TODO: send notification to users that are watching the contract and want highly tipped comments only // maybe TODO: send email notification to bet creator } @@ -576,7 +559,12 @@ export const createBetFillNotification = async ( contract: Contract, idempotencyKey: string ) => { - const { sendToBrowser } = await getDestinationsForUser(toUser.id, 'bet_fill') + const privateUser = await getPrivateUser(toUser.id) + if (!privateUser) return + const { sendToBrowser } = await getDestinationsForUser( + privateUser, + 'bet_fill' + ) if (!sendToBrowser) return const fill = userBet.fills.find((fill) => fill.matchedBetId === bet.id) @@ -616,8 +604,10 @@ export const createReferralNotification = async ( referredByContract?: Contract, referredByGroup?: Group ) => { + const privateUser = await getPrivateUser(toUser.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - toUser.id, + privateUser, 'you_referred_user' ) if (!sendToBrowser) return @@ -668,8 +658,10 @@ export const createLoanIncomeNotification = async ( idempotencyKey: string, income: number ) => { + const privateUser = await getPrivateUser(toUser.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - toUser.id, + privateUser, 'loan_income' ) if (!sendToBrowser) return @@ -704,8 +696,10 @@ export const createChallengeAcceptedNotification = async ( acceptedAmount: number, contract: Contract ) => { + const privateUser = await getPrivateUser(challengeCreator.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - challengeCreator.id, + privateUser, 'challenge_accepted' ) if (!sendToBrowser) return @@ -743,8 +737,10 @@ export const createBettingStreakBonusNotification = async ( amount: number, idempotencyKey: string ) => { + const privateUser = await getPrivateUser(user.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - user.id, + privateUser, 'betting_streak_incremented' ) if (!sendToBrowser) return @@ -784,8 +780,10 @@ export const createLikeNotification = async ( contract: Contract, tip?: TipTxn ) => { + const privateUser = await getPrivateUser(toUser.id) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - toUser.id, + privateUser, 'liked_and_tipped_your_contract' ) if (!sendToBrowser) return @@ -828,8 +826,11 @@ export const createUniqueBettorBonusNotification = async ( amount: number, idempotencyKey: string ) => { + console.log('createUniqueBettorBonusNotification') + const privateUser = await getPrivateUser(contractCreatorId) + if (!privateUser) return const { sendToBrowser } = await getDestinationsForUser( - contractCreatorId, + privateUser, 'unique_bettors_on_your_contract' ) if (!sendToBrowser) return diff --git a/functions/src/emails.ts b/functions/src/emails.ts index e4e5a540..e40a8ab8 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -18,12 +18,14 @@ import { formatNumericProbability } from '../../common/pseudo-numeric' import { sendTemplateEmail, sendTextEmail } from './send-email' import { getUser } from './utils' import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details' -import { notification_reason_types } from '../../common/notification' -import { getDestinationsForUser } from './create-notification' +import { + notification_reason_types, + getDestinationsForUser, +} from '../../common/notification' export const sendMarketResolutionEmail = async ( reason: notification_reason_types, - userId: string, + privateUser: PrivateUser, investment: number, payout: number, creator: User, @@ -33,14 +35,11 @@ export const sendMarketResolutionEmail = async ( resolutionProbability?: number, resolutions?: { [outcome: string]: number } ) => { - const { - privateUser, - sendToEmail, - urlToManageThisNotification: unsubscribeUrl, - } = await getDestinationsForUser(userId, reason) + const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } = + await getDestinationsForUser(privateUser, reason) if (!privateUser || !privateUser.email || !sendToEmail) return - const user = await getUser(userId) + const user = await getUser(privateUser.id) if (!user) return const outcome = toDisplayResolution( @@ -53,7 +52,7 @@ export const sendMarketResolutionEmail = async ( const subject = `Resolved ${outcome}: ${contract.question}` const creatorPayoutText = - creatorPayout >= 1 && userId === creator.id + creatorPayout >= 1 && privateUser.id === creator.id ? ` (plus ${formatMoney(creatorPayout)} in commissions)` : '' @@ -310,15 +309,13 @@ export const sendThankYouEmail = async ( export const sendMarketCloseEmail = async ( reason: notification_reason_types, user: User, + privateUser: PrivateUser, contract: Contract ) => { - const { - privateUser, - sendToEmail, - urlToManageThisNotification: unsubscribeUrl, - } = await getDestinationsForUser(user.id, reason) + const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } = + await getDestinationsForUser(privateUser, reason) - if (!privateUser || !privateUser.email || !sendToEmail) return + if (!privateUser.email || !sendToEmail) return const { username, name, id: userId } = user const firstName = name.split(' ')[0] @@ -344,7 +341,7 @@ export const sendMarketCloseEmail = async ( export const sendNewCommentEmail = async ( reason: notification_reason_types, - userId: string, + privateUser: PrivateUser, commentCreator: User, contract: Contract, commentText: string, @@ -353,11 +350,8 @@ export const sendNewCommentEmail = async ( answerText?: string, answerId?: string ) => { - const { - privateUser, - sendToEmail, - urlToManageThisNotification: unsubscribeUrl, - } = await getDestinationsForUser(userId, reason) + const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } = + await getDestinationsForUser(privateUser, reason) if (!privateUser || !privateUser.email || !sendToEmail) return const { question } = contract @@ -421,7 +415,7 @@ export const sendNewCommentEmail = async ( export const sendNewAnswerEmail = async ( reason: notification_reason_types, - userId: string, + privateUser: PrivateUser, name: string, text: string, contract: Contract, @@ -429,14 +423,11 @@ export const sendNewAnswerEmail = async ( ) => { const { creatorId } = contract // Don't send the creator's own answers. - if (userId === creatorId) return + if (privateUser.id === creatorId) return - const { - privateUser, - sendToEmail, - urlToManageThisNotification: unsubscribeUrl, - } = await getDestinationsForUser(userId, reason) - if (!privateUser || !privateUser.email || !sendToEmail) return + const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } = + await getDestinationsForUser(privateUser, reason) + if (!privateUser.email || !sendToEmail) return const { question, creatorUsername, slug } = contract diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 6f95818e..8b89379f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -1006,6 +1006,7 @@ function getReasonForShowingNotification( ) { const { sourceType, sourceUpdateType, reason, sourceSlug } = notification let reasonText: string + // TODO: we could leave out this switch and just use the reason field now that they have more information switch (sourceType) { case 'comment': if (reason === 'reply_to_users_answer')