diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts
index 2815655f..84edf715 100644
--- a/functions/src/create-notification.ts
+++ b/functions/src/create-notification.ts
@@ -22,7 +22,9 @@ import {
sendMarketResolutionEmail,
sendNewAnswerEmail,
sendNewCommentEmail,
+ sendNewFollowedMarketEmail,
} from './emails'
+import { filterDefined } from '../../common/util/array'
const firestore = admin.firestore()
type recipients_to_reason_texts = {
@@ -103,51 +105,14 @@ export const createNotification = async (
privateUser,
sourceContract
)
- } else if (reason === 'tagged_user') {
- // TODO: send email to tagged user in new contract
} else if (reason === 'subsidized_your_market') {
// TODO: send email to creator of market that was subsidized
- } else if (reason === 'contract_from_followed_user') {
- // TODO: send email to follower of user who created market
} else if (reason === 'on_new_follow') {
// TODO: send email to user who was followed
}
}
}
- const notifyUsersFollowers = async (
- userToReasonTexts: recipients_to_reason_texts
- ) => {
- const followers = await firestore
- .collectionGroup('follows')
- .where('userId', '==', sourceUser.id)
- .get()
-
- followers.docs.forEach((doc) => {
- const followerUserId = doc.ref.parent.parent?.id
- if (
- followerUserId &&
- shouldReceiveNotification(followerUserId, userToReasonTexts)
- ) {
- userToReasonTexts[followerUserId] = {
- reason: 'contract_from_followed_user',
- }
- }
- })
- }
-
- const notifyTaggedUsers = (
- userToReasonTexts: recipients_to_reason_texts,
- userIds: (string | undefined)[]
- ) => {
- userIds.forEach((id) => {
- if (id && shouldReceiveNotification(id, userToReasonTexts))
- userToReasonTexts[id] = {
- reason: 'tagged_user',
- }
- })
- }
-
// The following functions modify the userToReasonTexts object in place.
const userToReasonTexts: recipients_to_reason_texts = {}
@@ -157,15 +122,6 @@ export const createNotification = async (
reason: 'on_new_follow',
}
return await sendNotificationsIfSettingsPermit(userToReasonTexts)
- } else if (
- sourceType === 'contract' &&
- sourceUpdateType === 'created' &&
- sourceContract
- ) {
- if (sourceContract.visibility === 'public')
- await notifyUsersFollowers(userToReasonTexts)
- await notifyTaggedUsers(userToReasonTexts, recipients ?? [])
- return await sendNotificationsIfSettingsPermit(userToReasonTexts)
} else if (
sourceType === 'contract' &&
sourceUpdateType === 'closed' &&
@@ -283,52 +239,57 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
reason
)
+ // Browser notifications
if (sendToBrowser && !browserRecipientIdsList.includes(userId)) {
await createBrowserNotification(userId, reason)
browserRecipientIdsList.push(userId)
}
- if (sendToEmail && !emailRecipientIdsList.includes(userId)) {
- if (sourceType === 'comment') {
- const { repliedToType, repliedToAnswerText, repliedToId, bet } =
- repliedUsersInfo?.[userId] ?? {}
- // TODO: change subject of email title to be more specific, i.e.: replied to you on/tagged you on/comment
- await sendNewCommentEmail(
- reason,
- privateUser,
- sourceUser,
- sourceContract,
- sourceText,
- sourceId,
- bet,
- repliedToAnswerText,
- repliedToType === 'answer' ? repliedToId : undefined
- )
- } else if (sourceType === 'answer')
- await sendNewAnswerEmail(
- reason,
- privateUser,
- sourceUser.name,
- sourceText,
- sourceContract,
- sourceUser.avatarUrl
- )
- else if (
- sourceType === 'contract' &&
- sourceUpdateType === 'resolved' &&
- resolutionData
+
+ // Emails notifications
+ if (!sendToEmail || emailRecipientIdsList.includes(userId)) return
+ if (sourceType === 'comment') {
+ const { repliedToType, repliedToAnswerText, repliedToId, bet } =
+ repliedUsersInfo?.[userId] ?? {}
+ // TODO: change subject of email title to be more specific, i.e.: replied to you on/tagged you on/comment
+ await sendNewCommentEmail(
+ reason,
+ privateUser,
+ sourceUser,
+ sourceContract,
+ sourceText,
+ sourceId,
+ bet,
+ repliedToAnswerText,
+ repliedToType === 'answer' ? repliedToId : undefined
+ )
+ emailRecipientIdsList.push(userId)
+ } else if (sourceType === 'answer') {
+ await sendNewAnswerEmail(
+ reason,
+ privateUser,
+ sourceUser.name,
+ sourceText,
+ sourceContract,
+ sourceUser.avatarUrl
+ )
+ emailRecipientIdsList.push(userId)
+ } else if (
+ sourceType === 'contract' &&
+ sourceUpdateType === 'resolved' &&
+ resolutionData
+ ) {
+ await sendMarketResolutionEmail(
+ reason,
+ privateUser,
+ resolutionData.userInvestments[userId] ?? 0,
+ resolutionData.userPayouts[userId] ?? 0,
+ sourceUser,
+ resolutionData.creatorPayout,
+ sourceContract,
+ resolutionData.outcome,
+ resolutionData.resolutionProbability,
+ resolutionData.resolutions
)
- await sendMarketResolutionEmail(
- reason,
- privateUser,
- resolutionData.userInvestments[userId] ?? 0,
- resolutionData.userPayouts[userId] ?? 0,
- sourceUser,
- resolutionData.creatorPayout,
- sourceContract,
- resolutionData.outcome,
- resolutionData.resolutionProbability,
- resolutionData.resolutions
- )
emailRecipientIdsList.push(userId)
}
}
@@ -852,3 +813,79 @@ export const createUniqueBettorBonusNotification = async (
// TODO send email notification
}
+
+export const createNewContractNotification = async (
+ contractCreator: User,
+ contract: Contract,
+ idempotencyKey: string,
+ text: string,
+ mentionedUserIds: string[]
+) => {
+ if (contract.visibility !== 'public') return
+
+ const sendNotificationsIfSettingsAllow = async (
+ userId: string,
+ reason: notification_reason_types
+ ) => {
+ const privateUser = await getPrivateUser(userId)
+ if (!privateUser) return
+ const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
+ privateUser,
+ reason
+ )
+ if (sendToBrowser) {
+ const notificationRef = firestore
+ .collection(`/users/${userId}/notifications`)
+ .doc(idempotencyKey)
+ const notification: Notification = {
+ id: idempotencyKey,
+ userId: userId,
+ reason,
+ createdTime: Date.now(),
+ isSeen: false,
+ sourceId: contract.id,
+ sourceType: 'contract',
+ sourceUpdateType: 'created',
+ sourceUserName: contractCreator.name,
+ sourceUserUsername: contractCreator.username,
+ sourceUserAvatarUrl: contractCreator.avatarUrl,
+ sourceText: text,
+ sourceSlug: contract.slug,
+ sourceTitle: contract.question,
+ sourceContractSlug: contract.slug,
+ sourceContractId: contract.id,
+ sourceContractTitle: contract.question,
+ sourceContractCreatorUsername: contract.creatorUsername,
+ }
+ await notificationRef.set(removeUndefinedProps(notification))
+ }
+ if (!sendToEmail) return
+ if (reason === 'contract_from_followed_user')
+ await sendNewFollowedMarketEmail(reason, userId, privateUser, contract)
+ }
+ const followersSnapshot = await firestore
+ .collectionGroup('follows')
+ .where('userId', '==', contractCreator.id)
+ .get()
+
+ const followerUserIds = filterDefined(
+ followersSnapshot.docs.map((doc) => {
+ const followerUserId = doc.ref.parent.parent?.id
+ return followerUserId && followerUserId != contractCreator.id
+ ? followerUserId
+ : undefined
+ })
+ )
+
+ // As it is coded now, the tag notification usurps the new contract notification
+ // It'd be easy to append the reason to the eventId if desired
+ for (const followerUserId of followerUserIds) {
+ await sendNotificationsIfSettingsAllow(
+ followerUserId,
+ 'contract_from_followed_user'
+ )
+ }
+ for (const mentionedUserId of mentionedUserIds) {
+ await sendNotificationsIfSettingsAllow(mentionedUserId, 'tagged_user')
+ }
+}
diff --git a/functions/src/email-templates/new-market-from-followed-user.html b/functions/src/email-templates/new-market-from-followed-user.html
new file mode 100644
index 00000000..877d554f
--- /dev/null
+++ b/functions/src/email-templates/new-market-from-followed-user.html
@@ -0,0 +1,354 @@
+
+
+
+
+ New market from {{creatorName}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ {{creatorName}}, (who you're following) just created a new market, check it out!
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/functions/src/emails.ts b/functions/src/emails.ts
index d1387ef9..da6a5b41 100644
--- a/functions/src/emails.ts
+++ b/functions/src/emails.ts
@@ -510,3 +510,37 @@ function contractUrl(contract: Contract) {
function imageSourceUrl(contract: Contract) {
return buildCardUrl(getOpenGraphProps(contract))
}
+
+export const sendNewFollowedMarketEmail = async (
+ reason: notification_reason_types,
+ userId: string,
+ privateUser: PrivateUser,
+ contract: Contract
+) => {
+ const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
+ await getDestinationsForUser(privateUser, reason)
+ if (!privateUser.email || !sendToEmail) return
+ const user = await getUser(privateUser.id)
+ if (!user) return
+
+ const { name } = user
+ const firstName = name.split(' ')[0]
+ const creatorName = contract.creatorName
+
+ return await sendTemplateEmail(
+ privateUser.email,
+ `${creatorName} asked ${contract.question}`,
+ 'new-market-from-followed-user',
+ {
+ name: firstName,
+ creatorName,
+ unsubscribeUrl,
+ questionTitle: contract.question,
+ questionUrl: contractUrl(contract),
+ questionImgSrc: imageSourceUrl(contract),
+ },
+ {
+ from: `${creatorName} on Manifold `,
+ }
+ )
+}
diff --git a/functions/src/on-create-contract.ts b/functions/src/on-create-contract.ts
index d9826f6c..b613142b 100644
--- a/functions/src/on-create-contract.ts
+++ b/functions/src/on-create-contract.ts
@@ -1,7 +1,7 @@
import * as functions from 'firebase-functions'
import { getUser } from './utils'
-import { createNotification } from './create-notification'
+import { createNewContractNotification } from './create-notification'
import { Contract } from '../../common/contract'
import { parseMentions, richTextToString } from '../../common/util/parse'
import { JSONContent } from '@tiptap/core'
@@ -21,13 +21,11 @@ export const onCreateContract = functions
const mentioned = parseMentions(desc)
await addUserToContractFollowers(contract.id, contractCreator.id)
- await createNotification(
- contract.id,
- 'contract',
- 'created',
+ await createNewContractNotification(
contractCreator,
+ contract,
eventId,
richTextToString(desc),
- { contract, recipients: mentioned }
+ mentioned
)
})
diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx
index c319bf32..65804991 100644
--- a/web/components/notification-settings.tsx
+++ b/web/components/notification-settings.tsx
@@ -58,9 +58,9 @@ export function NotificationSettings(props: {
'onboarding_flow',
'thank_you_for_purchases',
+ 'tagged_user', // missing tagged on contract description email
+ 'contract_from_followed_user',
// TODO: add these
- 'tagged_user',
- // 'contract_from_followed_user',
// 'referral_bonuses',
// 'unique_bettors_on_your_contract',
// 'on_new_follow',
@@ -90,6 +90,7 @@ export function NotificationSettings(props: {
subscriptionTypeToDescription: {
all_comments_on_watched_markets: 'All new comments',
all_comments_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
+ // TODO: combine these two
all_replies_to_my_comments_on_watched_markets:
'Only replies to your comments',
all_replies_to_my_answers_on_watched_markets:
diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx
index a4c25ed3..7ebc473b 100644
--- a/web/pages/notifications.tsx
+++ b/web/pages/notifications.tsx
@@ -1031,52 +1031,53 @@ 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')
- reasonText = justSummary ? 'replied' : 'replied to you on'
- else if (reason === 'tagged_user')
- reasonText = justSummary ? 'tagged you' : 'tagged you on'
- else if (reason === 'reply_to_users_comment')
- reasonText = justSummary ? 'replied' : 'replied to you on'
- else reasonText = justSummary ? `commented` : `commented on`
- break
- case 'contract':
- if (reason === 'contract_from_followed_user')
- reasonText = justSummary ? 'asked the question' : 'asked'
- else if (sourceUpdateType === 'resolved')
- reasonText = justSummary ? `resolved the question` : `resolved`
- else if (sourceUpdateType === 'closed') reasonText = `Please resolve`
- else reasonText = justSummary ? 'updated the question' : `updated`
- break
- case 'answer':
- if (reason === 'answer_on_your_contract')
- reasonText = `answered your question `
- else reasonText = `answered`
- break
- case 'follow':
- reasonText = 'followed you'
- break
- case 'liquidity':
- reasonText = 'added a subsidy to your question'
- break
- case 'group':
- reasonText = 'added you to the group'
- break
- case 'user':
- if (sourceSlug && reason === 'user_joined_to_bet_on_your_market')
- reasonText = 'joined to bet on your market'
- else if (sourceSlug) reasonText = 'joined because you shared'
- else reasonText = 'joined because of you'
- break
- case 'bet':
- reasonText = 'bet against you'
- break
- case 'challenge':
- reasonText = 'accepted your challenge'
- break
- default:
- reasonText = ''
- }
+ if (reason === 'tagged_user')
+ reasonText = justSummary ? 'tagged you' : 'tagged you on'
+ else
+ switch (sourceType) {
+ case 'comment':
+ if (reason === 'reply_to_users_answer')
+ reasonText = justSummary ? 'replied' : 'replied to you on'
+ else if (reason === 'reply_to_users_comment')
+ reasonText = justSummary ? 'replied' : 'replied to you on'
+ else reasonText = justSummary ? `commented` : `commented on`
+ break
+ case 'contract':
+ if (reason === 'contract_from_followed_user')
+ reasonText = justSummary ? 'asked the question' : 'asked'
+ else if (sourceUpdateType === 'resolved')
+ reasonText = justSummary ? `resolved the question` : `resolved`
+ else if (sourceUpdateType === 'closed') reasonText = `Please resolve`
+ else reasonText = justSummary ? 'updated the question' : `updated`
+ break
+ case 'answer':
+ if (reason === 'answer_on_your_contract')
+ reasonText = `answered your question `
+ else reasonText = `answered`
+ break
+ case 'follow':
+ reasonText = 'followed you'
+ break
+ case 'liquidity':
+ reasonText = 'added a subsidy to your question'
+ break
+ case 'group':
+ reasonText = 'added you to the group'
+ break
+ case 'user':
+ if (sourceSlug && reason === 'user_joined_to_bet_on_your_market')
+ reasonText = 'joined to bet on your market'
+ else if (sourceSlug) reasonText = 'joined because you shared'
+ else reasonText = 'joined because of you'
+ break
+ case 'bet':
+ reasonText = 'bet against you'
+ break
+ case 'challenge':
+ reasonText = 'accepted your challenge'
+ break
+ default:
+ reasonText = ''
+ }
return reasonText
}