Direct & highlight sections for notif mgmt from emails

This commit is contained in:
Ian Philips 2022-09-09 14:34:51 -06:00
parent 4c4fe665ae
commit c1945b8ae9
16 changed files with 261 additions and 258 deletions

View File

@ -116,6 +116,8 @@ export type notification_subscription_types = {
contract_from_followed_user: notification_destination_types[] contract_from_followed_user: notification_destination_types[]
trending_markets: notification_destination_types[] trending_markets: notification_destination_types[]
profit_loss_updates: notification_destination_types[] profit_loss_updates: notification_destination_types[]
onboarding_flow: notification_destination_types[]
thank_you_for_purchases: notification_destination_types[]
} }
export type notification_subscribe_types = 'all' | 'less' | 'none' export type notification_subscribe_types = 'all' | 'less' | 'none'
@ -143,6 +145,7 @@ export const getDefaultNotificationSettings = (
unsubscribedFromAnswerEmails, unsubscribedFromAnswerEmails,
unsubscribedFromResolutionEmails, unsubscribedFromResolutionEmails,
unsubscribedFromWeeklyTrendingEmails, unsubscribedFromWeeklyTrendingEmails,
unsubscribedFromGenericEmails,
} = privateUser || {} } = privateUser || {}
const constructPref = (browserIf: boolean, emailIf: boolean) => { const constructPref = (browserIf: boolean, emailIf: boolean) => {
@ -258,5 +261,10 @@ export const getDefaultNotificationSettings = (
wantsAll || wantsLess, wantsAll || wantsLess,
false false
), ),
thank_you_for_purchases: constructPref(
false,
!unsubscribedFromGenericEmails
),
onboarding_flow: constructPref(false, !unsubscribedFromGenericEmails),
} as notification_subscription_types } as notification_subscription_types
} }

View File

@ -23,6 +23,7 @@ import {
sendNewAnswerEmail, sendNewAnswerEmail,
sendNewCommentEmail, sendNewCommentEmail,
} from './emails' } from './emails'
import { DOMAIN } from 'common/lib/envs/constants'
const firestore = admin.firestore() const firestore = admin.firestore()
type recipients_to_reason_texts = { type recipients_to_reason_texts = {
@ -92,7 +93,7 @@ export const createNotification = async (
if (!sendToEmail) continue if (!sendToEmail) continue
if (reason === 'your_contract_closed' && privateUser && sourceContract) { if (reason === 'your_contract_closed' && privateUser && sourceContract) {
await sendMarketCloseEmail(sourceUser, privateUser, sourceContract) await sendMarketCloseEmail(reason, sourceUser, sourceContract)
} else if (reason === 'tagged_user') { } else if (reason === 'tagged_user') {
// TODO: send email to tagged user in new contract // TODO: send email to tagged user in new contract
} else if (reason === 'subsidized_your_market') { } else if (reason === 'subsidized_your_market') {
@ -196,7 +197,7 @@ export const createNotification = async (
await sendNotificationsIfSettingsPermit(userToReasonTexts) await sendNotificationsIfSettingsPermit(userToReasonTexts)
} }
const getDestinationsForUser = async ( export const getDestinationsForUser = async (
userId: string, userId: string,
reason: notification_reason_types | keyof notification_subscription_types reason: notification_reason_types | keyof notification_subscription_types
) => { ) => {
@ -206,12 +207,13 @@ const getDestinationsForUser = async (
const notificationSettings = privateUser.notificationSubscriptionTypes const notificationSettings = privateUser.notificationSubscriptionTypes
let destinations let destinations
let subscriptionType: keyof notification_subscription_types | undefined
if (Object.keys(notificationSettings).includes(reason)) { if (Object.keys(notificationSettings).includes(reason)) {
const key = reason as keyof notification_subscription_types subscriptionType = reason as keyof notification_subscription_types
destinations = notificationSettings[key] destinations = notificationSettings[subscriptionType]
} else { } else {
const key = reason as notification_reason_types const key = reason as notification_reason_types
const subscriptionType = notificationReasonToSubscriptionType[key] subscriptionType = notificationReasonToSubscriptionType[key]
destinations = subscriptionType destinations = subscriptionType
? notificationSettings[subscriptionType] ? notificationSettings[subscriptionType]
: [] : []
@ -220,6 +222,7 @@ const getDestinationsForUser = async (
sendToEmail: destinations.includes('email'), sendToEmail: destinations.includes('email'),
sendToBrowser: destinations.includes('browser'), sendToBrowser: destinations.includes('browser'),
privateUser, privateUser,
urlToManageThisNotification: `${DOMAIN}/notifications?section=${subscriptionType}`,
} }
} }
@ -326,6 +329,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
if (sourceType === 'comment') { if (sourceType === 'comment') {
// if the source contract is a free response contract, send the email // if the source contract is a free response contract, send the email
await sendNewCommentEmail( await sendNewCommentEmail(
reason,
userId, userId,
sourceUser, sourceUser,
sourceContract, sourceContract,
@ -338,6 +342,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
) )
} else if (sourceType === 'answer') } else if (sourceType === 'answer')
await sendNewAnswerEmail( await sendNewAnswerEmail(
reason,
userId, userId,
sourceUser.name, sourceUser.name,
sourceText, sourceText,
@ -350,6 +355,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
resolutionData resolutionData
) )
await sendMarketResolutionEmail( await sendMarketResolutionEmail(
reason,
userId, userId,
resolutionData.userInvestments[userId], resolutionData.userInvestments[userId],
resolutionData.userPayouts[userId], resolutionData.userPayouts[userId],

View File

@ -284,9 +284,12 @@
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;"> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div <div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;"> style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a <p style="margin: 10px 0;">This e-mail has been sent to {{name}},
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;" <a href="{{unsubscribeLink}}" style="
target="_blank">click here to unsubscribe</a>.</p> color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
</p>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -493,7 +493,7 @@
<a href="{{unsubscribeLink}}" style=" <a href="{{unsubscribeLink}}" style="
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
" target="_blank">click here to unsubscribe</a>. " target="_blank">click here to manage your notifications</a>.
</p> </p>
</div> </div>
</td> </td>

View File

@ -440,11 +440,10 @@
<p style="margin: 10px 0"> <p style="margin: 10px 0">
This e-mail has been sent to This e-mail has been sent to
{{name}}, {{name}},
<a href="{{unsubscribeLink}}" <a href="{{unsubscribeLink}}" style="
style="
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
" target="_blank">click here to unsubscribe</a> from future recommended markets. " target="_blank">click here to manage your notifications</a>.
</p> </p>
</div> </div>
</td> </td>

View File

@ -526,19 +526,10 @@
" "
>our Discord</a >our Discord</a
>! Or, >! Or,
<a <a href="{{unsubscribeLink}}" style="
href="{{unsubscribeUrl}}" color: inherit;
style=" text-decoration: none;
font-family: 'Helvetica Neue', Helvetica, Arial, " target="_blank">click here to manage your notifications</a>.
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -366,15 +366,10 @@
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, ">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style=" <a href="{{unsubscribeLink}}" style="
font-family: 'Helvetica Neue', Helvetica, Arial, color: inherit;
sans-serif; text-decoration: none;
box-sizing: border-box; " target="_blank">click here to manage your notifications</a>.
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">unsubscribe</a>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -484,15 +484,10 @@
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, ">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style=" <a href="{{unsubscribeLink}}" style="
font-family: 'Helvetica Neue', Helvetica, Arial, color: inherit;
sans-serif; text-decoration: none;
box-sizing: border-box; " target="_blank">click here to manage your notifications</a>.
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">unsubscribe</a>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -366,15 +366,10 @@
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, ">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style=" <a href="{{unsubscribeLink}}" style="
font-family: 'Helvetica Neue', Helvetica, Arial, color: inherit;
sans-serif; text-decoration: none;
box-sizing: border-box; " target="_blank">click here to manage your notifications</a>.
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">unsubscribe</a>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -499,15 +499,10 @@
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
">our Discord</a>! Or, ">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style=" <a href="{{unsubscribeLink}}" style="
font-family: 'Helvetica Neue', Helvetica, Arial, color: inherit;
sans-serif; text-decoration: none;
box-sizing: border-box; " target="_blank">click here to manage your notifications</a>.
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">unsubscribe</a>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -487,15 +487,10 @@
> >
<p style="margin: 10px 0"> <p style="margin: 10px 0">
This e-mail has been sent to {{name}}, This e-mail has been sent to {{name}},
<a <a href="{{unsubscribeLink}}" style="
href="{{unsubscribeLink}}"
style="
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
" " target="_blank">click here to manage your notifications</a>.
target="_blank"
>click here to unsubscribe</a
>.
</p> </p>
</div> </div>
</td> </td>

View File

@ -214,10 +214,12 @@
<div <div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;"> style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
<p style="margin: 10px 0;">This e-mail has been sent <p style="margin: 10px 0;">This e-mail has been sent
to {{name}}, <a href="{{unsubscribeLink}}" to {{name}},
style="color:inherit;text-decoration:none;" <a href="{{unsubscribeLink}}" style="
target="_blank">click here to color: inherit;
unsubscribe</a>.</p> text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
</p>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -286,9 +286,12 @@
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;"> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div <div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;"> style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
<p style="margin: 10px 0;">This e-mail has been sent to {{name}}, <a <p style="margin: 10px 0;">This e-mail has been sent to {{name}},
href="{{unsubscribeLink}}” style=" color:inherit;text-decoration:none;" <a href="{{unsubscribeLink}}" style="
target="_blank">click here to unsubscribe</a>.</p> color: inherit;
text-decoration: none;
" target="_blank">click here to manage your notifications</a>.
</p>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -2,7 +2,11 @@ import { DOMAIN } from '../../common/envs/constants'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { getProbability } from '../../common/calculate' import { getProbability } from '../../common/calculate'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { PrivateUser, User } from '../../common/user' import {
notification_subscription_types,
PrivateUser,
User,
} from '../../common/user'
import { import {
formatLargeNumber, formatLargeNumber,
formatMoney, formatMoney,
@ -12,13 +16,13 @@ import { getValueFromBucket } from '../../common/calculate-dpm'
import { formatNumericProbability } from '../../common/pseudo-numeric' import { formatNumericProbability } from '../../common/pseudo-numeric'
import { sendTemplateEmail, sendTextEmail } from './send-email' import { sendTemplateEmail, sendTextEmail } from './send-email'
import { getPrivateUser, getUser } from './utils' import { getUser } from './utils'
import { getFunctionUrl } from '../../common/api'
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details' import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
import { notification_reason_types } from '../../common/notification'
const UNSUBSCRIBE_ENDPOINT = getFunctionUrl('unsubscribe') import { getDestinationsForUser } from './create-notification'
export const sendMarketResolutionEmail = async ( export const sendMarketResolutionEmail = async (
reason: notification_reason_types,
userId: string, userId: string,
investment: number, investment: number,
payout: number, payout: number,
@ -29,13 +33,12 @@ export const sendMarketResolutionEmail = async (
resolutionProbability?: number, resolutionProbability?: number,
resolutions?: { [outcome: string]: number } resolutions?: { [outcome: string]: number }
) => { ) => {
const privateUser = await getPrivateUser(userId) const {
if ( privateUser,
!privateUser || sendToEmail,
privateUser.unsubscribedFromResolutionEmails || urlToManageThisNotification: unsubscribeUrl,
!privateUser.email } = await getDestinationsForUser(userId, reason)
) if (!privateUser || !privateUser.email || !sendToEmail) return
return
const user = await getUser(userId) const user = await getUser(userId)
if (!user) return if (!user) return
@ -54,9 +57,6 @@ export const sendMarketResolutionEmail = async (
? ` (plus ${formatMoney(creatorPayout)} in commissions)` ? ` (plus ${formatMoney(creatorPayout)} in commissions)`
: '' : ''
const emailType = 'market-resolved'
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
const displayedInvestment = const displayedInvestment =
Number.isNaN(investment) || investment < 0 Number.isNaN(investment) || investment < 0
? formatMoney(0) ? formatMoney(0)
@ -151,11 +151,12 @@ export const sendWelcomeEmail = async (
) => { ) => {
if (!privateUser || !privateUser.email) return if (!privateUser || !privateUser.email) return
const { name, id: userId } = user const { name } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]
const emailType = 'generic' const unsubscribeLink = `${DOMAIN}/notifications?section=${
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` 'onboarding_flow' as keyof notification_subscription_types
}`
return await sendTemplateEmail( return await sendTemplateEmail(
privateUser.email, privateUser.email,
@ -214,16 +215,16 @@ export const sendOneWeekBonusEmail = async (
if ( if (
!privateUser || !privateUser ||
!privateUser.email || !privateUser.email ||
privateUser.unsubscribedFromGenericEmails !privateUser.notificationSubscriptionTypes.onboarding_flow.includes('email')
) )
return return
const { name, id: userId } = user const { name, id: userId } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]
const emailType = 'generic' const unsubscribeLink = `${DOMAIN}/notifications?section=${
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` 'onboarding_flow' as keyof notification_subscription_types
}`
return await sendTemplateEmail( return await sendTemplateEmail(
privateUser.email, privateUser.email,
'Manifold Markets one week anniversary gift', 'Manifold Markets one week anniversary gift',
@ -247,16 +248,16 @@ export const sendCreatorGuideEmail = async (
if ( if (
!privateUser || !privateUser ||
!privateUser.email || !privateUser.email ||
privateUser.unsubscribedFromGenericEmails !privateUser.notificationSubscriptionTypes.onboarding_flow.includes('email')
) )
return return
const { name, id: userId } = user const { name, id: userId } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]
const emailType = 'generic' const unsubscribeLink = `${DOMAIN}/notifications?section=${
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` 'onboarding_flow' as keyof notification_subscription_types
}`
return await sendTemplateEmail( return await sendTemplateEmail(
privateUser.email, privateUser.email,
'Create your own prediction market', 'Create your own prediction market',
@ -279,15 +280,18 @@ export const sendThankYouEmail = async (
if ( if (
!privateUser || !privateUser ||
!privateUser.email || !privateUser.email ||
privateUser.unsubscribedFromGenericEmails !privateUser.notificationSubscriptionTypes.thank_you_for_purchases.includes(
'email'
)
) )
return return
const { name, id: userId } = user const { name } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]
const emailType = 'generic' const unsubscribeLink = `${DOMAIN}/notifications?section=${
const unsubscribeLink = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}` 'thank_you_for_purchases' as keyof notification_subscription_types
}`
return await sendTemplateEmail( return await sendTemplateEmail(
privateUser.email, privateUser.email,
@ -304,16 +308,17 @@ export const sendThankYouEmail = async (
} }
export const sendMarketCloseEmail = async ( export const sendMarketCloseEmail = async (
reason: notification_reason_types,
user: User, user: User,
privateUser: PrivateUser,
contract: Contract contract: Contract
) => { ) => {
if ( const {
!privateUser || privateUser,
privateUser.unsubscribedFromResolutionEmails || sendToEmail,
!privateUser.email urlToManageThisNotification: unsubscribeUrl,
) } = await getDestinationsForUser(user.id, reason)
return
if (!privateUser || !privateUser.email || !sendToEmail) return
const { username, name, id: userId } = user const { username, name, id: userId } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]
@ -321,8 +326,6 @@ export const sendMarketCloseEmail = async (
const { question, slug, volume } = contract const { question, slug, volume } = contract
const url = `https://${DOMAIN}/${username}/${slug}` const url = `https://${DOMAIN}/${username}/${slug}`
const emailType = 'market-resolve'
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
return await sendTemplateEmail( return await sendTemplateEmail(
privateUser.email, privateUser.email,
@ -340,6 +343,7 @@ export const sendMarketCloseEmail = async (
} }
export const sendNewCommentEmail = async ( export const sendNewCommentEmail = async (
reason: notification_reason_types,
userId: string, userId: string,
commentCreator: User, commentCreator: User,
contract: Contract, contract: Contract,
@ -349,18 +353,15 @@ export const sendNewCommentEmail = async (
answerText?: string, answerText?: string,
answerId?: string answerId?: string
) => { ) => {
const privateUser = await getPrivateUser(userId) const {
if ( privateUser,
!privateUser || sendToEmail,
!privateUser.email || urlToManageThisNotification: unsubscribeUrl,
privateUser.unsubscribedFromCommentEmails } = await getDestinationsForUser(userId, reason)
) if (!privateUser || !privateUser.email || !sendToEmail) return
return
const { question } = contract const { question } = contract
const marketUrl = `https://${DOMAIN}/${contract.creatorUsername}/${contract.slug}#${commentId}` const marketUrl = `https://${DOMAIN}/${contract.creatorUsername}/${contract.slug}#${commentId}`
const emailType = 'market-comment'
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator const { name: commentorName, avatarUrl: commentorAvatarUrl } = commentCreator
@ -419,6 +420,7 @@ export const sendNewCommentEmail = async (
} }
export const sendNewAnswerEmail = async ( export const sendNewAnswerEmail = async (
reason: notification_reason_types,
userId: string, userId: string,
name: string, name: string,
text: string, text: string,
@ -429,19 +431,16 @@ export const sendNewAnswerEmail = async (
// Don't send the creator's own answers. // Don't send the creator's own answers.
if (userId === creatorId) return if (userId === creatorId) return
const privateUser = await getPrivateUser(userId) const {
if ( privateUser,
!privateUser || sendToEmail,
!privateUser.email || urlToManageThisNotification: unsubscribeUrl,
privateUser.unsubscribedFromAnswerEmails } = await getDestinationsForUser(userId, reason)
) if (!privateUser || !privateUser.email || !sendToEmail) return
return
const { question, creatorUsername, slug } = contract const { question, creatorUsername, slug } = contract
const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}` const marketUrl = `https://${DOMAIN}/${creatorUsername}/${slug}`
const emailType = 'market-answer'
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${userId}&type=${emailType}`
const subject = `New answer on ${question}` const subject = `New answer on ${question}`
const from = `${name} <info@manifold.markets>` const from = `${name} <info@manifold.markets>`
@ -470,12 +469,15 @@ export const sendInterestingMarketsEmail = async (
if ( if (
!privateUser || !privateUser ||
!privateUser.email || !privateUser.email ||
privateUser?.unsubscribedFromWeeklyTrendingEmails !privateUser.notificationSubscriptionTypes.trending_markets.includes(
'email'
)
) )
return return
const emailType = 'weekly-trending' const unsubscribeUrl = `${DOMAIN}/notifications?section=${
const unsubscribeUrl = `${UNSUBSCRIBE_ENDPOINT}?id=${privateUser.id}&type=${emailType}` 'trending_markets' as keyof notification_subscription_types
}`
const { name } = user const { name } = user
const firstName = name.split(' ')[0] const firstName = name.split(' ')[0]

View File

@ -27,7 +27,10 @@ import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
import { filterDefined } from 'common/util/array' import { filterDefined } from 'common/util/array'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
export function NotificationSettings() { export function NotificationSettings(props: {
navigateToSection: string | undefined
}) {
const { navigateToSection } = props
const privateUser = usePrivateUser() const privateUser = usePrivateUser()
const [showWatchModal, setShowWatchModal] = useState(false) const [showWatchModal, setShowWatchModal] = useState(false)
@ -53,6 +56,8 @@ export function NotificationSettings() {
'tagged_user', 'tagged_user',
'trending_markets', 'trending_markets',
'onboarding_flow',
'thank_you_for_purchases',
// TODO: add these // TODO: add these
// 'contract_from_followed_user', // 'contract_from_followed_user',
@ -67,77 +72,94 @@ export function NotificationSettings() {
// 'probability_updates_on_watched_markets', // 'probability_updates_on_watched_markets',
// 'limit_order_fills', // 'limit_order_fills',
] ]
const browserDisabled = ['trending_markets', 'profit_loss_updates'] const browserDisabled: Array<keyof notification_subscription_types> = [
'trending_markets',
'profit_loss_updates',
'onboarding_flow',
'thank_you_for_purchases',
]
const watched_markets_explanations_comments: { type sectionData = {
[key in keyof Partial<notification_subscription_types>]: string label: string
} = { subscriptionTypeToDescription: {
all_comments_on_watched_markets: 'All', [key in keyof Partial<notification_subscription_types>]: string
all_replies_to_my_comments_on_watched_markets: 'Replies to your comments', }
all_comments_on_contracts_with_shares_in_on_watched_markets:
'On markets you have shares in',
// comments_by_followed_users_on_watched_markets: 'By followed users',
}
const watched_markets_explanations_answers: {
[key in keyof Partial<notification_subscription_types>]: string
} = {
all_answers_on_watched_markets: 'All',
all_replies_to_my_answers_on_watched_markets: 'Replies to your answers',
all_answers_on_contracts_with_shares_in_on_watched_markets:
'On markets you have shares in',
// answers_by_followed_users_on_watched_markets: 'By followed users',
// answers_by_market_creator_on_watched_markets: 'By market creator',
}
const watched_markets_explanations_your_markets: {
[key in keyof Partial<notification_subscription_types>]: string
} = {
your_contract_closed: 'Your market has closed (and needs resolution)',
all_comments_on_my_markets: 'Comments on your markets',
all_answers_on_my_markets: 'Answers on your markets',
subsidized_your_market: 'Your market was subsidized',
tips_on_your_markets: 'Likes on your markets',
}
const watched_markets_explanations_market_updates: {
[key in keyof Partial<notification_subscription_types>]: string
} = {
market_updates_on_watched_markets: 'Updates made by the creator',
market_updates_on_watched_markets_with_shares_in:
'Updates made by the creator on markets you have shares in',
resolutions_on_watched_markets: 'Market resolutions',
resolutions_on_watched_markets_with_shares_in:
'Market resolutions you have shares in',
// probability_updates_on_watched_markets: 'Probability updates',
} }
const bonuses_explanations: { const comments: sectionData = {
[key in keyof Partial<notification_subscription_types>]: string label: 'New Comments',
} = { subscriptionTypeToDescription: {
betting_streaks: 'Betting streak bonuses', all_comments_on_watched_markets: 'All new comments',
referral_bonuses: 'Referral bonuses from referring users', all_comments_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets', all_replies_to_my_comments_on_watched_markets:
'Only replies to your comments',
// comments_by_followed_users_on_watched_markets: 'By followed users',
},
} }
const other_balance_change_explanations: { const answers: sectionData = {
[key in keyof Partial<notification_subscription_types>]: string label: 'New Answers',
} = { subscriptionTypeToDescription: {
loan_income: 'Automatic loans from your profitable bets', all_answers_on_watched_markets: 'All new answers',
limit_order_fills: 'Limit order fills', all_answers_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
tips_on_your_comments: 'Tips on your comments', all_replies_to_my_answers_on_watched_markets:
'Only replies to your answers',
// answers_by_followed_users_on_watched_markets: 'By followed users',
// answers_by_market_creator_on_watched_markets: 'By market creator',
},
} }
const updates: sectionData = {
const general_explanations: { label: 'Updates & Resolutions',
[key in keyof Partial<notification_subscription_types>]: string subscriptionTypeToDescription: {
} = { market_updates_on_watched_markets: 'All creator updates',
tagged_user: 'A user tagged you', market_updates_on_watched_markets_with_shares_in: `Only creator updates on markets you're invested in`,
trending_markets: 'Weekly trending markets', resolutions_on_watched_markets: 'All market resolutions',
// profit_loss_updates: 'Weekly profit/loss updates', resolutions_on_watched_markets_with_shares_in: `Only market resolutions you're invested in`,
// probability_updates_on_watched_markets: 'Probability updates',
},
} }
const yourMarkets: sectionData = {
const follows_and_followers_explanations: { label: 'Markets You Created',
[key in keyof Partial<notification_subscription_types>]: string subscriptionTypeToDescription: {
} = { your_contract_closed: 'Your market has closed (and needs resolution)',
on_new_follow: 'New followers', all_comments_on_my_markets: 'Comments on your markets',
contract_from_followed_user: 'New markets created by users you follow', all_answers_on_my_markets: 'Answers on your markets',
subsidized_your_market: 'Your market was subsidized',
tips_on_your_markets: 'Likes on your markets',
},
}
const bonuses: sectionData = {
label: 'Bonuses',
subscriptionTypeToDescription: {
betting_streaks: 'Betting streak bonuses',
referral_bonuses: 'Referral bonuses from referring users',
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
},
}
const otherBalances: sectionData = {
label: 'Other',
subscriptionTypeToDescription: {
loan_income: 'Automatic loans from your profitable bets',
limit_order_fills: 'Limit order fills',
tips_on_your_comments: 'Tips on your comments',
},
}
const userInteractions: sectionData = {
label: 'Users',
subscriptionTypeToDescription: {
tagged_user: 'A user tagged you',
on_new_follow: 'Someone followed you',
contract_from_followed_user: 'New markets created by users you follow',
},
}
const generalOther: sectionData = {
label: 'Other',
subscriptionTypeToDescription: {
trending_markets: 'Weekly interesting markets',
thank_you_for_purchases: 'Thank you notes for your purchases',
onboarding_flow: 'Explanatory emails to help you get started',
// profit_loss_updates: 'Weekly profit/loss updates',
},
} }
const NotificationSettingLine = ( const NotificationSettingLine = (
@ -151,6 +173,7 @@ export function NotificationSettings() {
const [emailEnabled, setEmailEnabled] = useState(previousEmailValue) const [emailEnabled, setEmailEnabled] = useState(previousEmailValue)
const loading = 'Changing Notifications Settings' const loading = 'Changing Notifications Settings'
const success = 'Changed Notification Settings!' const success = 'Changed Notification Settings!'
const highlight = navigateToSection === key
useEffect(() => { useEffect(() => {
if ( if (
@ -183,7 +206,12 @@ export function NotificationSettings() {
]) ])
return ( return (
<Row className={clsx('my-1 gap-1 text-gray-300')}> <Row
className={clsx(
'my-1 gap-1 text-gray-300',
highlight ? 'rounded-md bg-indigo-100 p-1' : ''
)}
>
<Col className="ml-3 gap-2 text-sm"> <Col className="ml-3 gap-2 text-sm">
<Row className="gap-2 font-medium text-gray-700"> <Row className="gap-2 font-medium text-gray-700">
<span>{description}</span> <span>{description}</span>
@ -251,16 +279,20 @@ export function NotificationSettings() {
return privateUser.notificationSubscriptionTypes[key] ?? [] return privateUser.notificationSubscriptionTypes[key] ?? []
} }
const Section = ( const Section = (icon: ReactNode, data: sectionData) => {
icon: ReactNode, const { label, subscriptionTypeToDescription } = data
label: string, const expand =
subscriptionTypeToDescription: { navigateToSection &&
[key in keyof Partial<notification_subscription_types>]: string Object.keys(subscriptionTypeToDescription).includes(navigateToSection)
} const [expanded, setExpanded] = useState(expand)
) => {
const [expanded, setExpanded] = useState(false) // Not working as the default value for expanded, so using a useEffect
useEffect(() => {
if (expand) setExpanded(true)
}, [expand])
return ( return (
<Col className={'ml-2 gap-2'}> <Col className={clsx('ml-2 gap-2')}>
<Row <Row
className={'mt-1 cursor-pointer items-center gap-2 text-gray-600'} className={'mt-1 cursor-pointer items-center gap-2 text-gray-600'}
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
@ -303,52 +335,20 @@ export function NotificationSettings() {
onClick={() => setShowWatchModal(true)} onClick={() => setShowWatchModal(true)}
/> />
</Row> </Row>
{Section( {Section(<ChatIcon className={'h-6 w-6'} />, comments)}
<ChatIcon className={'h-6 w-6'} />, {Section(<LightBulbIcon className={'h-6 w-6'} />, answers)}
'New Comments', {Section(<TrendingUpIcon className={'h-6 w-6'} />, updates)}
watched_markets_explanations_comments {Section(<UserIcon className={'h-6 w-6'} />, yourMarkets)}
)}
{Section(
<LightBulbIcon className={'h-6 w-6'} />,
'New Answers',
watched_markets_explanations_answers
)}
{Section(
<TrendingUpIcon className={'h-6 w-6'} />,
'Updates & Resolutions',
watched_markets_explanations_market_updates
)}
{Section(
<UserIcon className={'h-6 w-6'} />,
'Markets You Created',
watched_markets_explanations_your_markets
)}
<Row className={'gap-2 text-xl text-gray-700'}> <Row className={'gap-2 text-xl text-gray-700'}>
<span>Balance Changes</span> <span>Balance Changes</span>
</Row> </Row>
{Section( {Section(<CurrencyDollarIcon className={'h-6 w-6'} />, bonuses)}
<CurrencyDollarIcon className={'h-6 w-6'} />, {Section(<CashIcon className={'h-6 w-6'} />, otherBalances)}
'Bonuses',
bonuses_explanations
)}
{Section(
<CashIcon className={'h-6 w-6'} />,
'Other',
other_balance_change_explanations
)}
<Row className={'gap-2 text-xl text-gray-700'}> <Row className={'gap-2 text-xl text-gray-700'}>
<span>General</span> <span>General</span>
</Row> </Row>
{Section( {Section(<UsersIcon className={'h-6 w-6'} />, userInteractions)}
<UsersIcon className={'h-6 w-6'} />, {Section(<InboxInIcon className={'h-6 w-6'} />, generalOther)}
'Follows & Followers',
follows_and_followers_explanations
)}
{Section(
<InboxInIcon className={'h-6 w-6'} />,
'Other',
general_explanations
)}
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} /> <WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
</Col> </Col>
</div> </div>

View File

@ -1,6 +1,6 @@
import { Tabs } from 'web/components/layout/tabs' import { ControlledTabs } from 'web/components/layout/tabs'
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useState } from 'react'
import Router from 'next/router' import Router, { useRouter } from 'next/router'
import { Notification, notification_source_types } from 'common/notification' import { Notification, notification_source_types } from 'common/notification'
import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Avatar, EmptyAvatar } from 'web/components/avatar'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
@ -56,24 +56,38 @@ const HIGHLIGHT_CLASS = 'bg-indigo-50'
export default function Notifications() { export default function Notifications() {
const privateUser = usePrivateUser() const privateUser = usePrivateUser()
const router = useRouter()
const [navigateToSection, setNavigateToSection] = useState<string>()
const [activeIndex, setActiveIndex] = useState(0)
useEffect(() => { useEffect(() => {
if (privateUser === null) Router.push('/') if (privateUser === null) Router.push('/')
}) })
useEffect(() => {
const query = { ...router.query }
if (query.section) {
setNavigateToSection(query.section as string)
setActiveIndex(1)
}
}, [router.query])
return ( return (
<Page> <Page>
<div className={'px-2 pt-4 sm:px-4 lg:pt-0'}> <div className={'px-2 pt-4 sm:px-4 lg:pt-0'}>
<Title text={'Notifications'} className={'hidden md:block'} /> <Title text={'Notifications'} className={'hidden md:block'} />
<SEO title="Notifications" description="Manifold user notifications" /> <SEO title="Notifications" description="Manifold user notifications" />
{privateUser && ( {privateUser && router.isReady && (
<div> <div>
<Tabs <ControlledTabs
currentPageForAnalytics={'notifications'} currentPageForAnalytics={'notifications'}
labelClassName={'pb-2 pt-1 '} labelClassName={'pb-2 pt-1 '}
className={'mb-0 sm:mb-2'} className={'mb-0 sm:mb-2'}
defaultIndex={0} activeIndex={activeIndex}
onClick={(title, index) => {
setActiveIndex(index)
}}
tabs={[ tabs={[
{ {
title: 'Notifications', title: 'Notifications',
@ -82,9 +96,9 @@ export default function Notifications() {
{ {
title: 'Settings', title: 'Settings',
content: ( content: (
<div className={''}> <NotificationSettings
<NotificationSettings /> navigateToSection={navigateToSection}
</div> />
), ),
}, },
]} ]}