Allow one-click unsubscribe, slight refactor
This commit is contained in:
parent
68b0539fc1
commit
3efd968058
|
@ -1,5 +1,4 @@
|
||||||
import { notification_subscription_types, PrivateUser } from './user'
|
import { notification_preference } from './user-notification-preferences'
|
||||||
import { DOMAIN } from './envs/constants'
|
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -29,6 +28,7 @@ export type Notification = {
|
||||||
|
|
||||||
isSeenOnHref?: string
|
isSeenOnHref?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_source_types =
|
export type notification_source_types =
|
||||||
| 'contract'
|
| 'contract'
|
||||||
| 'comment'
|
| 'comment'
|
||||||
|
@ -54,7 +54,7 @@ export type notification_source_update_types =
|
||||||
| 'deleted'
|
| 'deleted'
|
||||||
| 'closed'
|
| 'closed'
|
||||||
|
|
||||||
/* Optional - if possible use a keyof notification_subscription_types */
|
/* Optional - if possible use a notification_preference */
|
||||||
export type notification_reason_types =
|
export type notification_reason_types =
|
||||||
| 'tagged_user'
|
| 'tagged_user'
|
||||||
| 'on_new_follow'
|
| 'on_new_follow'
|
||||||
|
@ -92,75 +92,152 @@ export type notification_reason_types =
|
||||||
| 'your_contract_closed'
|
| 'your_contract_closed'
|
||||||
| 'subsidized_your_market'
|
| 'subsidized_your_market'
|
||||||
|
|
||||||
// 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<notification_reason_types, keyof notification_subscription_types>
|
|
||||||
> = {
|
|
||||||
you_referred_user: 'referral_bonuses',
|
|
||||||
user_joined_to_bet_on_your_market: 'referral_bonuses',
|
|
||||||
tip_received: 'tips_on_your_comments',
|
|
||||||
bet_fill: 'limit_order_fills',
|
|
||||||
user_joined_from_your_group_invite: 'referral_bonuses',
|
|
||||||
challenge_accepted: 'limit_order_fills',
|
|
||||||
betting_streak_incremented: 'betting_streaks',
|
|
||||||
liked_and_tipped_your_contract: 'tips_on_your_markets',
|
|
||||||
comment_on_your_contract: 'all_comments_on_my_markets',
|
|
||||||
answer_on_your_contract: 'all_answers_on_my_markets',
|
|
||||||
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
|
|
||||||
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
|
|
||||||
update_on_contract_you_follow: 'market_updates_on_watched_markets',
|
|
||||||
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
|
|
||||||
comment_on_contract_with_users_shares_in:
|
|
||||||
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
|
||||||
answer_on_contract_with_users_shares_in:
|
|
||||||
'all_answers_on_contracts_with_shares_in_on_watched_markets',
|
|
||||||
update_on_contract_with_users_shares_in:
|
|
||||||
'market_updates_on_watched_markets_with_shares_in',
|
|
||||||
resolution_on_contract_with_users_shares_in:
|
|
||||||
'resolutions_on_watched_markets_with_shares_in',
|
|
||||||
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
|
|
||||||
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
|
|
||||||
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
|
|
||||||
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
|
|
||||||
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
|
|
||||||
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
|
|
||||||
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
|
|
||||||
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
|
|
||||||
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.notificationPreferences
|
|
||||||
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]
|
|
||||||
: []
|
|
||||||
}
|
|
||||||
// const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
|
|
||||||
return {
|
|
||||||
sendToEmail: destinations.includes('email'),
|
|
||||||
sendToBrowser: destinations.includes('browser'),
|
|
||||||
// unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
|
|
||||||
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BettingStreakData = {
|
export type BettingStreakData = {
|
||||||
streak: number
|
streak: number
|
||||||
bonusAmount: number
|
bonusAmount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type notification_descriptions = {
|
||||||
|
[key in notification_preference]: {
|
||||||
|
simple: string
|
||||||
|
detailed: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
|
||||||
|
all_answers_on_my_markets: {
|
||||||
|
simple: 'Answers on your markets',
|
||||||
|
detailed: 'Answers on your own markets',
|
||||||
|
},
|
||||||
|
all_comments_on_my_markets: {
|
||||||
|
simple: 'Comments on your markets',
|
||||||
|
detailed: 'Comments on your own markets',
|
||||||
|
},
|
||||||
|
answers_by_followed_users_on_watched_markets: {
|
||||||
|
simple: 'Only answers by users you follow',
|
||||||
|
detailed: "Only answers by users you follow on markets you're watching",
|
||||||
|
},
|
||||||
|
answers_by_market_creator_on_watched_markets: {
|
||||||
|
simple: 'Only answers by market creator',
|
||||||
|
detailed: "Only answers by market creator on markets you're watching",
|
||||||
|
},
|
||||||
|
betting_streaks: {
|
||||||
|
simple: 'For predictions made over consecutive days',
|
||||||
|
detailed: 'Bonuses for predictions made over consecutive days',
|
||||||
|
},
|
||||||
|
comments_by_followed_users_on_watched_markets: {
|
||||||
|
simple: 'Only comments by users you follow',
|
||||||
|
detailed:
|
||||||
|
'Only comments by users that you follow on markets that you watch',
|
||||||
|
},
|
||||||
|
contract_from_followed_user: {
|
||||||
|
simple: 'New markets from users you follow',
|
||||||
|
detailed: 'New markets from users you follow',
|
||||||
|
},
|
||||||
|
limit_order_fills: {
|
||||||
|
simple: 'Limit order fills',
|
||||||
|
detailed: 'When your limit order is filled by another user',
|
||||||
|
},
|
||||||
|
loan_income: {
|
||||||
|
simple: 'Automatic loans from your predictions in unresolved markets',
|
||||||
|
detailed:
|
||||||
|
'Automatic loans from your predictions that are locked in unresolved markets',
|
||||||
|
},
|
||||||
|
market_updates_on_watched_markets: {
|
||||||
|
simple: 'All creator updates',
|
||||||
|
detailed: 'All market updates made by the creator',
|
||||||
|
},
|
||||||
|
market_updates_on_watched_markets_with_shares_in: {
|
||||||
|
simple: "Only creator updates on markets that you're invested in",
|
||||||
|
detailed:
|
||||||
|
"Only updates made by the creator on markets that you're invested in",
|
||||||
|
},
|
||||||
|
on_new_follow: {
|
||||||
|
simple: 'A user followed you',
|
||||||
|
detailed: 'A user followed you',
|
||||||
|
},
|
||||||
|
onboarding_flow: {
|
||||||
|
simple: 'Emails to help you get started using Manifold',
|
||||||
|
detailed: 'Emails to help you learn how to use Manifold',
|
||||||
|
},
|
||||||
|
probability_updates_on_watched_markets: {
|
||||||
|
simple: 'Large changes in probability on markets that you watch',
|
||||||
|
detailed: 'Large changes in probability on markets that you watch',
|
||||||
|
},
|
||||||
|
profit_loss_updates: {
|
||||||
|
simple: 'Weekly profit and loss updates',
|
||||||
|
detailed: 'Weekly profit and loss updates',
|
||||||
|
},
|
||||||
|
referral_bonuses: {
|
||||||
|
simple: 'For referring new users',
|
||||||
|
detailed: 'Bonuses you receive from referring a new user',
|
||||||
|
},
|
||||||
|
resolutions_on_watched_markets: {
|
||||||
|
simple: 'All market resolutions',
|
||||||
|
detailed: "All resolutions on markets that you're watching",
|
||||||
|
},
|
||||||
|
resolutions_on_watched_markets_with_shares_in: {
|
||||||
|
simple: "Only market resolutions that you're invested in",
|
||||||
|
detailed:
|
||||||
|
"Only resolutions of markets you're watching and that you're invested in",
|
||||||
|
},
|
||||||
|
subsidized_your_market: {
|
||||||
|
simple: 'Your market was subsidized',
|
||||||
|
detailed: 'When someone subsidizes your market',
|
||||||
|
},
|
||||||
|
tagged_user: {
|
||||||
|
simple: 'A user tagged you',
|
||||||
|
detailed: 'When another use tags you',
|
||||||
|
},
|
||||||
|
thank_you_for_purchases: {
|
||||||
|
simple: 'Thank you notes for your purchases',
|
||||||
|
detailed: 'Thank you notes for your purchases',
|
||||||
|
},
|
||||||
|
tipped_comments_on_watched_markets: {
|
||||||
|
simple: 'Only highly tipped comments on markets that you watch',
|
||||||
|
detailed: 'Only highly tipped comments on markets that you watch',
|
||||||
|
},
|
||||||
|
tips_on_your_comments: {
|
||||||
|
simple: 'Tips on your comments',
|
||||||
|
detailed: 'Tips on your comments',
|
||||||
|
},
|
||||||
|
tips_on_your_markets: {
|
||||||
|
simple: 'Tips/Likes on your markets',
|
||||||
|
detailed: 'Tips/Likes on your markets',
|
||||||
|
},
|
||||||
|
trending_markets: {
|
||||||
|
simple: 'Weekly interesting markets',
|
||||||
|
detailed: 'Weekly interesting markets',
|
||||||
|
},
|
||||||
|
unique_bettors_on_your_contract: {
|
||||||
|
simple: 'For unique predictors on your markets',
|
||||||
|
detailed: 'Bonuses for unique predictors on your markets',
|
||||||
|
},
|
||||||
|
your_contract_closed: {
|
||||||
|
simple: 'Your market has closed and you need to resolve it',
|
||||||
|
detailed: 'Your market has closed and you need to resolve it',
|
||||||
|
},
|
||||||
|
all_comments_on_watched_markets: {
|
||||||
|
simple: 'All new comments',
|
||||||
|
detailed: 'All new comments on markets you follow',
|
||||||
|
},
|
||||||
|
all_comments_on_contracts_with_shares_in_on_watched_markets: {
|
||||||
|
simple: `Only on markets you're invested in`,
|
||||||
|
detailed: `Comments on markets that you're watching and you're invested in`,
|
||||||
|
},
|
||||||
|
all_replies_to_my_comments_on_watched_markets: {
|
||||||
|
simple: 'Only replies to your comments',
|
||||||
|
detailed: "Only replies to your comments on markets you're watching",
|
||||||
|
},
|
||||||
|
all_replies_to_my_answers_on_watched_markets: {
|
||||||
|
simple: 'Only replies to your answers',
|
||||||
|
detailed: "Only replies to your answers on markets you're watching",
|
||||||
|
},
|
||||||
|
all_answers_on_watched_markets: {
|
||||||
|
simple: 'All new answers',
|
||||||
|
detailed: "All new answers on markets you're watching",
|
||||||
|
},
|
||||||
|
all_answers_on_contracts_with_shares_in_on_watched_markets: {
|
||||||
|
simple: `Only on markets you're invested in`,
|
||||||
|
detailed: `Answers on markets that you're watching and that you're invested in`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
243
common/user-notification-preferences.ts
Normal file
243
common/user-notification-preferences.ts
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import { filterDefined } from './util/array'
|
||||||
|
import { notification_reason_types } from './notification'
|
||||||
|
import { getFunctionUrl } from './api'
|
||||||
|
import { DOMAIN } from './envs/constants'
|
||||||
|
import { PrivateUser } from './user'
|
||||||
|
|
||||||
|
export type notification_destination_types = 'email' | 'browser'
|
||||||
|
export type notification_preference = keyof notification_preferences
|
||||||
|
export type notification_preferences = {
|
||||||
|
// Watched Markets
|
||||||
|
all_comments_on_watched_markets: notification_destination_types[]
|
||||||
|
all_answers_on_watched_markets: notification_destination_types[]
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
tipped_comments_on_watched_markets: notification_destination_types[]
|
||||||
|
comments_by_followed_users_on_watched_markets: notification_destination_types[]
|
||||||
|
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
|
||||||
|
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
|
||||||
|
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
||||||
|
|
||||||
|
// Answers
|
||||||
|
answers_by_followed_users_on_watched_markets: notification_destination_types[]
|
||||||
|
answers_by_market_creator_on_watched_markets: notification_destination_types[]
|
||||||
|
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
||||||
|
|
||||||
|
// On users' markets
|
||||||
|
your_contract_closed: notification_destination_types[]
|
||||||
|
all_comments_on_my_markets: notification_destination_types[]
|
||||||
|
all_answers_on_my_markets: notification_destination_types[]
|
||||||
|
subsidized_your_market: notification_destination_types[]
|
||||||
|
|
||||||
|
// Market updates
|
||||||
|
resolutions_on_watched_markets: notification_destination_types[]
|
||||||
|
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
|
||||||
|
market_updates_on_watched_markets: notification_destination_types[]
|
||||||
|
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
|
||||||
|
probability_updates_on_watched_markets: notification_destination_types[]
|
||||||
|
|
||||||
|
// Balance Changes
|
||||||
|
loan_income: notification_destination_types[]
|
||||||
|
betting_streaks: notification_destination_types[]
|
||||||
|
referral_bonuses: notification_destination_types[]
|
||||||
|
unique_bettors_on_your_contract: notification_destination_types[]
|
||||||
|
tips_on_your_comments: notification_destination_types[]
|
||||||
|
tips_on_your_markets: notification_destination_types[]
|
||||||
|
limit_order_fills: notification_destination_types[]
|
||||||
|
|
||||||
|
// General
|
||||||
|
tagged_user: notification_destination_types[]
|
||||||
|
on_new_follow: notification_destination_types[]
|
||||||
|
contract_from_followed_user: notification_destination_types[]
|
||||||
|
trending_markets: notification_destination_types[]
|
||||||
|
profit_loss_updates: notification_destination_types[]
|
||||||
|
onboarding_flow: notification_destination_types[]
|
||||||
|
thank_you_for_purchases: notification_destination_types[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultNotificationPreferences = (
|
||||||
|
userId: string,
|
||||||
|
privateUser?: PrivateUser,
|
||||||
|
noEmails?: boolean
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
unsubscribedFromCommentEmails,
|
||||||
|
unsubscribedFromAnswerEmails,
|
||||||
|
unsubscribedFromResolutionEmails,
|
||||||
|
unsubscribedFromWeeklyTrendingEmails,
|
||||||
|
unsubscribedFromGenericEmails,
|
||||||
|
} = privateUser || {}
|
||||||
|
|
||||||
|
const constructPref = (browserIf: boolean, emailIf: boolean) => {
|
||||||
|
const browser = browserIf ? 'browser' : undefined
|
||||||
|
const email = noEmails ? undefined : emailIf ? 'email' : undefined
|
||||||
|
return filterDefined([browser, email]) as notification_destination_types[]
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
// Watched Markets
|
||||||
|
all_comments_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
all_answers_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromAnswerEmails
|
||||||
|
),
|
||||||
|
|
||||||
|
// Comments
|
||||||
|
tips_on_your_comments: constructPref(true, !unsubscribedFromCommentEmails),
|
||||||
|
comments_by_followed_users_on_watched_markets: constructPref(true, false),
|
||||||
|
all_replies_to_my_comments_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
all_replies_to_my_answers_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
|
||||||
|
// Answers
|
||||||
|
answers_by_followed_users_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromAnswerEmails
|
||||||
|
),
|
||||||
|
answers_by_market_creator_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromAnswerEmails
|
||||||
|
),
|
||||||
|
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromAnswerEmails
|
||||||
|
),
|
||||||
|
|
||||||
|
// On users' markets
|
||||||
|
your_contract_closed: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromResolutionEmails
|
||||||
|
), // High priority
|
||||||
|
all_comments_on_my_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
all_answers_on_my_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromAnswerEmails
|
||||||
|
),
|
||||||
|
subsidized_your_market: constructPref(true, true),
|
||||||
|
|
||||||
|
// Market updates
|
||||||
|
resolutions_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromResolutionEmails
|
||||||
|
),
|
||||||
|
market_updates_on_watched_markets: constructPref(true, false),
|
||||||
|
market_updates_on_watched_markets_with_shares_in: constructPref(
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
),
|
||||||
|
resolutions_on_watched_markets_with_shares_in: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromResolutionEmails
|
||||||
|
),
|
||||||
|
|
||||||
|
//Balance Changes
|
||||||
|
loan_income: constructPref(true, false),
|
||||||
|
betting_streaks: constructPref(true, false),
|
||||||
|
referral_bonuses: constructPref(true, true),
|
||||||
|
unique_bettors_on_your_contract: constructPref(true, false),
|
||||||
|
tipped_comments_on_watched_markets: constructPref(
|
||||||
|
true,
|
||||||
|
!unsubscribedFromCommentEmails
|
||||||
|
),
|
||||||
|
tips_on_your_markets: constructPref(true, true),
|
||||||
|
limit_order_fills: constructPref(true, false),
|
||||||
|
|
||||||
|
// General
|
||||||
|
tagged_user: constructPref(true, true),
|
||||||
|
on_new_follow: constructPref(true, true),
|
||||||
|
contract_from_followed_user: constructPref(true, true),
|
||||||
|
trending_markets: constructPref(
|
||||||
|
false,
|
||||||
|
!unsubscribedFromWeeklyTrendingEmails
|
||||||
|
),
|
||||||
|
profit_loss_updates: constructPref(false, true),
|
||||||
|
probability_updates_on_watched_markets: constructPref(true, false),
|
||||||
|
thank_you_for_purchases: constructPref(
|
||||||
|
false,
|
||||||
|
!unsubscribedFromGenericEmails
|
||||||
|
),
|
||||||
|
onboarding_flow: constructPref(false, !unsubscribedFromGenericEmails),
|
||||||
|
} as notification_preferences
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const notificationReasonToSubscriptionType: Partial<
|
||||||
|
Record<notification_reason_types, notification_preference>
|
||||||
|
> = {
|
||||||
|
you_referred_user: 'referral_bonuses',
|
||||||
|
user_joined_to_bet_on_your_market: 'referral_bonuses',
|
||||||
|
tip_received: 'tips_on_your_comments',
|
||||||
|
bet_fill: 'limit_order_fills',
|
||||||
|
user_joined_from_your_group_invite: 'referral_bonuses',
|
||||||
|
challenge_accepted: 'limit_order_fills',
|
||||||
|
betting_streak_incremented: 'betting_streaks',
|
||||||
|
liked_and_tipped_your_contract: 'tips_on_your_markets',
|
||||||
|
comment_on_your_contract: 'all_comments_on_my_markets',
|
||||||
|
answer_on_your_contract: 'all_answers_on_my_markets',
|
||||||
|
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
|
||||||
|
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
|
||||||
|
update_on_contract_you_follow: 'market_updates_on_watched_markets',
|
||||||
|
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
|
||||||
|
comment_on_contract_with_users_shares_in:
|
||||||
|
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
||||||
|
answer_on_contract_with_users_shares_in:
|
||||||
|
'all_answers_on_contracts_with_shares_in_on_watched_markets',
|
||||||
|
update_on_contract_with_users_shares_in:
|
||||||
|
'market_updates_on_watched_markets_with_shares_in',
|
||||||
|
resolution_on_contract_with_users_shares_in:
|
||||||
|
'resolutions_on_watched_markets_with_shares_in',
|
||||||
|
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
|
||||||
|
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
|
||||||
|
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
|
||||||
|
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
|
||||||
|
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
|
||||||
|
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
|
||||||
|
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
|
||||||
|
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
|
||||||
|
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 getNotificationDestinationsForUser = (
|
||||||
|
privateUser: PrivateUser,
|
||||||
|
reason: notification_reason_types | notification_preference
|
||||||
|
) => {
|
||||||
|
const notificationSettings = privateUser.notificationPreferences
|
||||||
|
let destinations
|
||||||
|
let subscriptionType: notification_preference | undefined
|
||||||
|
if (Object.keys(notificationSettings).includes(reason)) {
|
||||||
|
subscriptionType = reason as notification_preference
|
||||||
|
destinations = notificationSettings[subscriptionType]
|
||||||
|
} else {
|
||||||
|
const key = reason as notification_reason_types
|
||||||
|
subscriptionType = notificationReasonToSubscriptionType[key]
|
||||||
|
destinations = subscriptionType
|
||||||
|
? notificationSettings[subscriptionType]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
|
||||||
|
return {
|
||||||
|
sendToEmail: destinations.includes('email'),
|
||||||
|
sendToBrowser: destinations.includes('browser'),
|
||||||
|
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
|
||||||
|
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings§ion=${subscriptionType}`,
|
||||||
|
}
|
||||||
|
}
|
174
common/user.ts
174
common/user.ts
|
@ -1,4 +1,4 @@
|
||||||
import { filterDefined } from './util/array'
|
import { notification_preferences } from './user-notification-preferences'
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -65,7 +65,7 @@ export type PrivateUser = {
|
||||||
initialDeviceToken?: string
|
initialDeviceToken?: string
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
notificationPreferences: notification_subscription_types
|
notificationPreferences: notification_preferences
|
||||||
twitchInfo?: {
|
twitchInfo?: {
|
||||||
twitchName: string
|
twitchName: string
|
||||||
controlToken: string
|
controlToken: string
|
||||||
|
@ -73,57 +73,6 @@ export type PrivateUser = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_destination_types = 'email' | 'browser'
|
|
||||||
export type notification_subscription_types = {
|
|
||||||
// Watched Markets
|
|
||||||
all_comments_on_watched_markets: notification_destination_types[]
|
|
||||||
all_answers_on_watched_markets: notification_destination_types[]
|
|
||||||
|
|
||||||
// Comments
|
|
||||||
tipped_comments_on_watched_markets: notification_destination_types[]
|
|
||||||
comments_by_followed_users_on_watched_markets: notification_destination_types[]
|
|
||||||
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
|
|
||||||
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
|
|
||||||
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
|
||||||
|
|
||||||
// Answers
|
|
||||||
answers_by_followed_users_on_watched_markets: notification_destination_types[]
|
|
||||||
answers_by_market_creator_on_watched_markets: notification_destination_types[]
|
|
||||||
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
|
|
||||||
|
|
||||||
// On users' markets
|
|
||||||
your_contract_closed: notification_destination_types[]
|
|
||||||
all_comments_on_my_markets: notification_destination_types[]
|
|
||||||
all_answers_on_my_markets: notification_destination_types[]
|
|
||||||
subsidized_your_market: notification_destination_types[]
|
|
||||||
|
|
||||||
// Market updates
|
|
||||||
resolutions_on_watched_markets: notification_destination_types[]
|
|
||||||
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
|
|
||||||
market_updates_on_watched_markets: notification_destination_types[]
|
|
||||||
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
|
|
||||||
probability_updates_on_watched_markets: notification_destination_types[]
|
|
||||||
|
|
||||||
// Balance Changes
|
|
||||||
loan_income: notification_destination_types[]
|
|
||||||
betting_streaks: notification_destination_types[]
|
|
||||||
referral_bonuses: notification_destination_types[]
|
|
||||||
unique_bettors_on_your_contract: notification_destination_types[]
|
|
||||||
tips_on_your_comments: notification_destination_types[]
|
|
||||||
tips_on_your_markets: notification_destination_types[]
|
|
||||||
limit_order_fills: notification_destination_types[]
|
|
||||||
|
|
||||||
// General
|
|
||||||
tagged_user: notification_destination_types[]
|
|
||||||
on_new_follow: notification_destination_types[]
|
|
||||||
contract_from_followed_user: notification_destination_types[]
|
|
||||||
trending_markets: 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 PortfolioMetrics = {
|
export type PortfolioMetrics = {
|
||||||
investmentValue: number
|
investmentValue: number
|
||||||
balance: number
|
balance: number
|
||||||
|
@ -134,122 +83,3 @@ export type PortfolioMetrics = {
|
||||||
|
|
||||||
export const MANIFOLD_USERNAME = 'ManifoldMarkets'
|
export const MANIFOLD_USERNAME = 'ManifoldMarkets'
|
||||||
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
|
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
|
||||||
|
|
||||||
export const getDefaultNotificationSettings = (
|
|
||||||
userId: string,
|
|
||||||
privateUser?: PrivateUser,
|
|
||||||
noEmails?: boolean
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
unsubscribedFromCommentEmails,
|
|
||||||
unsubscribedFromAnswerEmails,
|
|
||||||
unsubscribedFromResolutionEmails,
|
|
||||||
unsubscribedFromWeeklyTrendingEmails,
|
|
||||||
unsubscribedFromGenericEmails,
|
|
||||||
} = privateUser || {}
|
|
||||||
|
|
||||||
const constructPref = (browserIf: boolean, emailIf: boolean) => {
|
|
||||||
const browser = browserIf ? 'browser' : undefined
|
|
||||||
const email = noEmails ? undefined : emailIf ? 'email' : undefined
|
|
||||||
return filterDefined([browser, email]) as notification_destination_types[]
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
// Watched Markets
|
|
||||||
all_comments_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
all_answers_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromAnswerEmails
|
|
||||||
),
|
|
||||||
|
|
||||||
// Comments
|
|
||||||
tips_on_your_comments: constructPref(true, !unsubscribedFromCommentEmails),
|
|
||||||
comments_by_followed_users_on_watched_markets: constructPref(true, false),
|
|
||||||
all_replies_to_my_comments_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
all_replies_to_my_answers_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
|
|
||||||
// Answers
|
|
||||||
answers_by_followed_users_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromAnswerEmails
|
|
||||||
),
|
|
||||||
answers_by_market_creator_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromAnswerEmails
|
|
||||||
),
|
|
||||||
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromAnswerEmails
|
|
||||||
),
|
|
||||||
|
|
||||||
// On users' markets
|
|
||||||
your_contract_closed: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromResolutionEmails
|
|
||||||
), // High priority
|
|
||||||
all_comments_on_my_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
all_answers_on_my_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromAnswerEmails
|
|
||||||
),
|
|
||||||
subsidized_your_market: constructPref(true, true),
|
|
||||||
|
|
||||||
// Market updates
|
|
||||||
resolutions_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromResolutionEmails
|
|
||||||
),
|
|
||||||
market_updates_on_watched_markets: constructPref(true, false),
|
|
||||||
market_updates_on_watched_markets_with_shares_in: constructPref(
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
),
|
|
||||||
resolutions_on_watched_markets_with_shares_in: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromResolutionEmails
|
|
||||||
),
|
|
||||||
|
|
||||||
//Balance Changes
|
|
||||||
loan_income: constructPref(true, false),
|
|
||||||
betting_streaks: constructPref(true, false),
|
|
||||||
referral_bonuses: constructPref(true, true),
|
|
||||||
unique_bettors_on_your_contract: constructPref(true, false),
|
|
||||||
tipped_comments_on_watched_markets: constructPref(
|
|
||||||
true,
|
|
||||||
!unsubscribedFromCommentEmails
|
|
||||||
),
|
|
||||||
tips_on_your_markets: constructPref(true, true),
|
|
||||||
limit_order_fills: constructPref(true, false),
|
|
||||||
|
|
||||||
// General
|
|
||||||
tagged_user: constructPref(true, true),
|
|
||||||
on_new_follow: constructPref(true, true),
|
|
||||||
contract_from_followed_user: constructPref(true, true),
|
|
||||||
trending_markets: constructPref(
|
|
||||||
false,
|
|
||||||
!unsubscribedFromWeeklyTrendingEmails
|
|
||||||
),
|
|
||||||
profit_loss_updates: constructPref(false, true),
|
|
||||||
probability_updates_on_watched_markets: constructPref(true, false),
|
|
||||||
thank_you_for_purchases: constructPref(
|
|
||||||
false,
|
|
||||||
!unsubscribedFromGenericEmails
|
|
||||||
),
|
|
||||||
onboarding_flow: constructPref(false, !unsubscribedFromGenericEmails),
|
|
||||||
} as notification_subscription_types
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import {
|
import {
|
||||||
BettingStreakData,
|
BettingStreakData,
|
||||||
getDestinationsForUser,
|
|
||||||
Notification,
|
Notification,
|
||||||
notification_reason_types,
|
notification_reason_types,
|
||||||
} from '../../common/notification'
|
} from '../../common/notification'
|
||||||
|
@ -27,6 +26,7 @@ import {
|
||||||
sendNewUniqueBettorsEmail,
|
sendNewUniqueBettorsEmail,
|
||||||
} from './emails'
|
} from './emails'
|
||||||
import { filterDefined } from '../../common/util/array'
|
import { filterDefined } from '../../common/util/array'
|
||||||
|
import { getNotificationDestinationsForUser } from '../../common/user-notification-preferences'
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
type recipients_to_reason_texts = {
|
type recipients_to_reason_texts = {
|
||||||
|
@ -66,7 +66,7 @@ export const createNotification = async (
|
||||||
const { reason } = userToReasonTexts[userId]
|
const { reason } = userToReasonTexts[userId]
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (!privateUser) continue
|
if (!privateUser) continue
|
||||||
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
reason
|
reason
|
||||||
)
|
)
|
||||||
|
@ -236,7 +236,7 @@ export const createCommentOrAnswerOrUpdatedContractNotification = async (
|
||||||
return
|
return
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
reason
|
reason
|
||||||
)
|
)
|
||||||
|
@ -468,7 +468,7 @@ export const createTipNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'tip_received'
|
'tip_received'
|
||||||
)
|
)
|
||||||
|
@ -513,7 +513,7 @@ export const createBetFillNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'bet_fill'
|
'bet_fill'
|
||||||
)
|
)
|
||||||
|
@ -558,7 +558,7 @@ export const createReferralNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'you_referred_user'
|
'you_referred_user'
|
||||||
)
|
)
|
||||||
|
@ -612,7 +612,7 @@ export const createLoanIncomeNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'loan_income'
|
'loan_income'
|
||||||
)
|
)
|
||||||
|
@ -650,7 +650,7 @@ export const createChallengeAcceptedNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(challengeCreator.id)
|
const privateUser = await getPrivateUser(challengeCreator.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'challenge_accepted'
|
'challenge_accepted'
|
||||||
)
|
)
|
||||||
|
@ -692,7 +692,7 @@ export const createBettingStreakBonusNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(user.id)
|
const privateUser = await getPrivateUser(user.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'betting_streak_incremented'
|
'betting_streak_incremented'
|
||||||
)
|
)
|
||||||
|
@ -739,7 +739,7 @@ export const createLikeNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(toUser.id)
|
const privateUser = await getPrivateUser(toUser.id)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser } = await getDestinationsForUser(
|
const { sendToBrowser } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'liked_and_tipped_your_contract'
|
'liked_and_tipped_your_contract'
|
||||||
)
|
)
|
||||||
|
@ -786,7 +786,7 @@ export const createUniqueBettorBonusNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(contractCreatorId)
|
const privateUser = await getPrivateUser(contractCreatorId)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
'unique_bettors_on_your_contract'
|
'unique_bettors_on_your_contract'
|
||||||
)
|
)
|
||||||
|
@ -876,7 +876,7 @@ export const createNewContractNotification = async (
|
||||||
) => {
|
) => {
|
||||||
const privateUser = await getPrivateUser(userId)
|
const privateUser = await getPrivateUser(userId)
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
const { sendToBrowser, sendToEmail } = await getDestinationsForUser(
|
const { sendToBrowser, sendToEmail } = getNotificationDestinationsForUser(
|
||||||
privateUser,
|
privateUser,
|
||||||
reason
|
reason
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import {
|
import { PrivateUser, User } from '../../common/user'
|
||||||
getDefaultNotificationSettings,
|
|
||||||
PrivateUser,
|
|
||||||
User,
|
|
||||||
} from '../../common/user'
|
|
||||||
import { getUser, getUserByUsername, getValues } from './utils'
|
import { getUser, getUserByUsername, getValues } from './utils'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import {
|
import {
|
||||||
|
@ -22,6 +18,7 @@ import { track } from './analytics'
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { Group } from '../../common/group'
|
import { Group } from '../../common/group'
|
||||||
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
|
import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
|
||||||
|
import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
deviceToken: z.string().optional(),
|
deviceToken: z.string().optional(),
|
||||||
|
@ -83,7 +80,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
|
||||||
email,
|
email,
|
||||||
initialIpAddress: req.ip,
|
initialIpAddress: req.ip,
|
||||||
initialDeviceToken: deviceToken,
|
initialDeviceToken: deviceToken,
|
||||||
notificationPreferences: getDefaultNotificationSettings(auth.uid),
|
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
await firestore.collection('private-users').doc(auth.uid).create(privateUser)
|
||||||
|
|
|
@ -1,321 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
|
||||||
<!--[if !mso]><!-->
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<!--<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
||||||
<style type="text/css">
|
|
||||||
#outlook a {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
|
||||||
td {
|
|
||||||
border-collapse: collapse;
|
|
||||||
mso-table-lspace: 0pt;
|
|
||||||
mso-table-rspace: 0pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
height: auto;
|
|
||||||
line-height: 100%;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
-ms-interpolation-mode: bicubic;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: block;
|
|
||||||
margin: 13px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!--[if mso]>
|
|
||||||
<noscript>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
</noscript>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.mj-outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (min-width:480px) {
|
|
||||||
.mj-column-per-100 {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style media="screen and (min-width:480px)">
|
|
||||||
.moz-text-html .mj-column-per-100 {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style type="text/css">
|
|
||||||
[owa] .mj-column-per-100 {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (max-width:480px) {
|
|
||||||
table.mj-full-width-mobile {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.mj-full-width-mobile {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="word-spacing:normal;background-color:#F4F4F4;">
|
|
||||||
<div style="background-color:#F4F4F4;">
|
|
||||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="background:#ffffff;background-color:#ffffff;width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center"
|
|
||||||
style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
|
|
||||||
alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif"
|
|
||||||
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
|
||||||
width="550"></a></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left"
|
|
||||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
|
||||||
<p class="text-build-content"
|
|
||||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
|
||||||
data-testid="4XoHRGw1Y"><span
|
|
||||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
|
||||||
Hi {{name}},</span></p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left"
|
|
||||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
|
||||||
<p class="text-build-content"
|
|
||||||
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
|
||||||
data-testid="4XoHRGw1Y"><span
|
|
||||||
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
|
|
||||||
using Manifold Markets. Running low
|
|
||||||
on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p></p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center">
|
|
||||||
<table cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<table cellspacing="0" cellpadding="0">
|
|
||||||
<tr>
|
|
||||||
<td style="border-radius: 2px;" bgcolor="#4337c9">
|
|
||||||
<a href="{{manalink}}" target="_blank"
|
|
||||||
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
|
|
||||||
Claim M$500
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left"
|
|
||||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
|
||||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
|
||||||
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
|
|
||||||
you know, besides making correct predictions, there are
|
|
||||||
plenty of other ways to earn mana?</span></p>
|
|
||||||
<ul>
|
|
||||||
<li style="line-height:23px;"><span
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
|
|
||||||
tips on comments</span></li>
|
|
||||||
<li style="line-height:23px;"><span
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;">Unique
|
|
||||||
trader bonus for each user who bets on your
|
|
||||||
markets</span></li>
|
|
||||||
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
|
|
||||||
class="link-build-content" style="color:inherit;; text-decoration: none;"
|
|
||||||
target="_blank" href="https://manifold.markets/referrals"><span
|
|
||||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
|
|
||||||
friends</u></span></a></span></li>
|
|
||||||
<li style="line-height:23px;"><a class="link-build-content"
|
|
||||||
style="color:inherit;; text-decoration: none;" target="_blank"
|
|
||||||
href="https://manifold.markets/group/bugs?s=most-traded"><span
|
|
||||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
|
|
||||||
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
|
|
||||||
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
|
|
||||||
target="_blank"
|
|
||||||
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
|
|
||||||
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
|
|
||||||
feedback</u></span></a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
|
||||||
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
|
||||||
style="color:#000000;font-family:Arial;font-size:18px;">David
|
|
||||||
from Manifold</span></p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq"
|
|
||||||
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left"
|
|
||||||
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
|
||||||
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
|
|
||||||
data-testid="3Q8BP69fq"></a></li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"> </p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
|
||||||
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span></p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
|
|
||||||
style="color:#000000;font-family:Arial;font-size:18px;">David from Manifold</span></p>
|
|
||||||
<p class="text-build-content" data-testid="3Q8BP69fq"
|
|
||||||
style="margin: 10px 0; margin-bottom: 10px;"> </p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
<div style="margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
|
||||||
<div style="margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
|
|
||||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="vertical-align:top;padding:0;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td align="center"
|
|
||||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
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 href="{{unsubscribeUrl}}" style="
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
" target="_blank">click here to manage your notifications</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center"
|
|
||||||
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -494,7 +494,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -443,7 +443,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -529,7 +529,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -369,7 +369,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -487,7 +487,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -369,7 +369,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -470,7 +470,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -502,7 +502,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -318,7 +318,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -376,7 +376,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -480,7 +480,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -283,7 +283,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -218,7 +218,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -290,7 +290,7 @@
|
||||||
<a href="{{unsubscribeUrl}}" style="
|
<a href="{{unsubscribeUrl}}" 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 from this type of notification</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -2,11 +2,7 @@ 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 {
|
import { PrivateUser, User } from '../../common/user'
|
||||||
notification_subscription_types,
|
|
||||||
PrivateUser,
|
|
||||||
User,
|
|
||||||
} from '../../common/user'
|
|
||||||
import {
|
import {
|
||||||
formatLargeNumber,
|
formatLargeNumber,
|
||||||
formatMoney,
|
formatMoney,
|
||||||
|
@ -18,11 +14,12 @@ import { formatNumericProbability } from '../../common/pseudo-numeric'
|
||||||
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
import { sendTemplateEmail, sendTextEmail } from './send-email'
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
import { buildCardUrl, getOpenGraphProps } from '../../common/contract-details'
|
||||||
import {
|
import { notification_reason_types } from '../../common/notification'
|
||||||
notification_reason_types,
|
|
||||||
getDestinationsForUser,
|
|
||||||
} from '../../common/notification'
|
|
||||||
import { Dictionary } from 'lodash'
|
import { Dictionary } from 'lodash'
|
||||||
|
import {
|
||||||
|
getNotificationDestinationsForUser,
|
||||||
|
notification_preference,
|
||||||
|
} from '../../common/user-notification-preferences'
|
||||||
|
|
||||||
export const sendMarketResolutionEmail = async (
|
export const sendMarketResolutionEmail = async (
|
||||||
reason: notification_reason_types,
|
reason: notification_reason_types,
|
||||||
|
@ -36,8 +33,10 @@ export const sendMarketResolutionEmail = async (
|
||||||
resolutionProbability?: number,
|
resolutionProbability?: number,
|
||||||
resolutions?: { [outcome: string]: number }
|
resolutions?: { [outcome: string]: number }
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
const user = await getUser(privateUser.id)
|
const user = await getUser(privateUser.id)
|
||||||
|
@ -154,7 +153,7 @@ export const sendWelcomeEmail = async (
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
||||||
'onboarding_flow' as keyof notification_subscription_types
|
'onboarding_flow' as notification_preference
|
||||||
}`
|
}`
|
||||||
|
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
|
@ -222,7 +221,7 @@ export const sendOneWeekBonusEmail = async (
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
||||||
'onboarding_flow' as keyof notification_subscription_types
|
'onboarding_flow' as notification_preference
|
||||||
}`
|
}`
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
@ -255,7 +254,7 @@ export const sendCreatorGuideEmail = async (
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
||||||
'onboarding_flow' as keyof notification_subscription_types
|
'onboarding_flow' as notification_preference
|
||||||
}`
|
}`
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
privateUser.email,
|
privateUser.email,
|
||||||
|
@ -289,7 +288,7 @@ export const sendThankYouEmail = async (
|
||||||
const firstName = name.split(' ')[0]
|
const firstName = name.split(' ')[0]
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
||||||
'thank_you_for_purchases' as keyof notification_subscription_types
|
'thank_you_for_purchases' as notification_preference
|
||||||
}`
|
}`
|
||||||
|
|
||||||
return await sendTemplateEmail(
|
return await sendTemplateEmail(
|
||||||
|
@ -312,8 +311,10 @@ export const sendMarketCloseEmail = async (
|
||||||
privateUser: PrivateUser,
|
privateUser: PrivateUser,
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
|
|
||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
|
@ -350,8 +351,10 @@ export const sendNewCommentEmail = async (
|
||||||
answerText?: string,
|
answerText?: string,
|
||||||
answerId?: string
|
answerId?: string
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
if (!privateUser || !privateUser.email || !sendToEmail) return
|
if (!privateUser || !privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
const { question } = contract
|
const { question } = contract
|
||||||
|
@ -425,8 +428,10 @@ export const sendNewAnswerEmail = async (
|
||||||
// Don't send the creator's own answers.
|
// Don't send the creator's own answers.
|
||||||
if (privateUser.id === creatorId) return
|
if (privateUser.id === creatorId) return
|
||||||
|
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
const { question, creatorUsername, slug } = contract
|
const { question, creatorUsername, slug } = contract
|
||||||
|
@ -465,7 +470,7 @@ export const sendInterestingMarketsEmail = async (
|
||||||
return
|
return
|
||||||
|
|
||||||
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
const unsubscribeUrl = `${DOMAIN}/notifications?tab=settings§ion=${
|
||||||
'trending_markets' as keyof notification_subscription_types
|
'trending_markets' as notification_preference
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const { name } = user
|
const { name } = user
|
||||||
|
@ -516,8 +521,10 @@ export const sendNewFollowedMarketEmail = async (
|
||||||
privateUser: PrivateUser,
|
privateUser: PrivateUser,
|
||||||
contract: Contract
|
contract: Contract
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
const user = await getUser(privateUser.id)
|
const user = await getUser(privateUser.id)
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
@ -553,8 +560,10 @@ export const sendNewUniqueBettorsEmail = async (
|
||||||
userBets: Dictionary<[Bet, ...Bet[]]>,
|
userBets: Dictionary<[Bet, ...Bet[]]>,
|
||||||
bonusAmount: number
|
bonusAmount: number
|
||||||
) => {
|
) => {
|
||||||
const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } =
|
const { sendToEmail, unsubscribeUrl } = getNotificationDestinationsForUser(
|
||||||
await getDestinationsForUser(privateUser, reason)
|
privateUser,
|
||||||
|
reason
|
||||||
|
)
|
||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
const user = await getUser(privateUser.id)
|
const user = await getUser(privateUser.id)
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
|
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
import { getDefaultNotificationSettings } from 'common/user'
|
|
||||||
import { getAllPrivateUsers, isProd } from 'functions/src/utils'
|
import { getAllPrivateUsers, isProd } from 'functions/src/utils'
|
||||||
|
import { getDefaultNotificationPreferences } from 'common/user-notification-preferences'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
@ -17,7 +17,7 @@ async function main() {
|
||||||
.collection('private-users')
|
.collection('private-users')
|
||||||
.doc(privateUser.id)
|
.doc(privateUser.id)
|
||||||
.update({
|
.update({
|
||||||
notificationPreferences: getDefaultNotificationSettings(
|
notificationPreferences: getDefaultNotificationPreferences(
|
||||||
privateUser.id,
|
privateUser.id,
|
||||||
privateUser,
|
privateUser,
|
||||||
disableEmails
|
disableEmails
|
||||||
|
|
|
@ -3,8 +3,9 @@ import * as admin from 'firebase-admin'
|
||||||
import { initAdmin } from './script-init'
|
import { initAdmin } from './script-init'
|
||||||
initAdmin()
|
initAdmin()
|
||||||
|
|
||||||
import { getDefaultNotificationSettings, PrivateUser, User } from 'common/user'
|
import { PrivateUser, User } from 'common/user'
|
||||||
import { STARTING_BALANCE } from 'common/economy'
|
import { STARTING_BALANCE } from 'common/economy'
|
||||||
|
import { getDefaultNotificationPreferences } from 'common/user-notification-preferences'
|
||||||
|
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ async function main() {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
email,
|
email,
|
||||||
username,
|
username,
|
||||||
notificationPreferences: getDefaultNotificationSettings(user.id),
|
notificationPreferences: getDefaultNotificationPreferences(user.id),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.totalDeposits === undefined) {
|
if (user.totalDeposits === undefined) {
|
||||||
|
|
|
@ -1,79 +1,227 @@
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { EndpointDefinition } from './api'
|
import { EndpointDefinition } from './api'
|
||||||
import { getUser } from './utils'
|
import { getPrivateUser } from './utils'
|
||||||
import { PrivateUser } from '../../common/user'
|
import { PrivateUser } from '../../common/user'
|
||||||
|
import { NOTIFICATION_DESCRIPTIONS } from '../../common/notification'
|
||||||
|
import { notification_preference } from '../../common/user-notification-preferences'
|
||||||
|
|
||||||
export const unsubscribe: EndpointDefinition = {
|
export const unsubscribe: EndpointDefinition = {
|
||||||
opts: { method: 'GET', minInstances: 1 },
|
opts: { method: 'GET', minInstances: 1 },
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
const id = req.query.id as string
|
const id = req.query.id as string
|
||||||
let type = req.query.type as string
|
const type = req.query.type as string
|
||||||
if (!id || !type) {
|
if (!id || !type) {
|
||||||
res.status(400).send('Empty id or type parameter.')
|
res.status(400).send('Empty id or subscription type parameter.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(`Unsubscribing ${id} from ${type}`)
|
||||||
|
const notificationSubscriptionType = type as notification_preference
|
||||||
|
if (notificationSubscriptionType === undefined) {
|
||||||
|
res.status(400).send('Invalid subscription type parameter.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'market-resolved') type = 'market-resolve'
|
const user = await getPrivateUser(id)
|
||||||
|
|
||||||
if (
|
|
||||||
![
|
|
||||||
'market-resolve',
|
|
||||||
'market-comment',
|
|
||||||
'market-answer',
|
|
||||||
'generic',
|
|
||||||
'weekly-trending',
|
|
||||||
].includes(type)
|
|
||||||
) {
|
|
||||||
res.status(400).send('Invalid type parameter.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getUser(id)
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.send('This user is not currently subscribed or does not exist.')
|
res.send('This user is not currently subscribed or does not exist.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name } = user
|
const previousDestinations =
|
||||||
|
user.notificationPreferences[notificationSubscriptionType]
|
||||||
|
|
||||||
|
console.log(previousDestinations)
|
||||||
|
const { email } = user
|
||||||
|
|
||||||
const update: Partial<PrivateUser> = {
|
const update: Partial<PrivateUser> = {
|
||||||
...(type === 'market-resolve' && {
|
notificationPreferences: {
|
||||||
unsubscribedFromResolutionEmails: true,
|
...user.notificationPreferences,
|
||||||
}),
|
[notificationSubscriptionType]: previousDestinations.filter(
|
||||||
...(type === 'market-comment' && {
|
(destination) => destination !== 'email'
|
||||||
unsubscribedFromCommentEmails: true,
|
),
|
||||||
}),
|
},
|
||||||
...(type === 'market-answer' && {
|
|
||||||
unsubscribedFromAnswerEmails: true,
|
|
||||||
}),
|
|
||||||
...(type === 'generic' && {
|
|
||||||
unsubscribedFromGenericEmails: true,
|
|
||||||
}),
|
|
||||||
...(type === 'weekly-trending' && {
|
|
||||||
unsubscribedFromWeeklyTrendingEmails: true,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await firestore.collection('private-users').doc(id).update(update)
|
await firestore.collection('private-users').doc(id).update(update)
|
||||||
|
|
||||||
if (type === 'market-resolve')
|
res.send(
|
||||||
res.send(
|
`
|
||||||
`${name}, you have been unsubscribed from market resolution emails on Manifold Markets.`
|
<!DOCTYPE html>
|
||||||
)
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
||||||
else if (type === 'market-comment')
|
xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
res.send(
|
|
||||||
`${name}, you have been unsubscribed from market comment emails on Manifold Markets.`
|
<head>
|
||||||
)
|
<title>Manifold Markets 7th Day Anniversary Gift!</title>
|
||||||
else if (type === 'market-answer')
|
<!--[if !mso]><!-->
|
||||||
res.send(
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
`${name}, you have been unsubscribed from market answer emails on Manifold Markets.`
|
<!--<![endif]-->
|
||||||
)
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
else if (type === 'weekly-trending')
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
res.send(
|
<style type="text/css">
|
||||||
`${name}, you have been unsubscribed from weekly trending emails on Manifold Markets.`
|
#outlook a {
|
||||||
)
|
padding: 0;
|
||||||
else res.send(`${name}, you have been unsubscribed.`)
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
[owa] .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
table.mj-full-width-mobile {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mj-full-width-mobile {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="word-spacing:normal;background-color:#F4F4F4;">
|
||||||
|
<div style="background-color:#F4F4F4;">
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
||||||
|
style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix"
|
||||||
|
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
||||||
|
width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="width:550px;">
|
||||||
|
<a href="https://manifold.markets" target="_blank">
|
||||||
|
<img alt="banner logo" height="auto" src="https://manifold.markets/logo-banner.png"
|
||||||
|
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
|
||||||
|
title="" width="550">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y"><span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
Hello!</span></p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
|
||||||
|
<div
|
||||||
|
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
|
||||||
|
<p class="text-build-content"
|
||||||
|
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
|
||||||
|
data-testid="4XoHRGw1Y">
|
||||||
|
<span
|
||||||
|
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
|
||||||
|
${email} has been unsubscribed from email notifications related to:
|
||||||
|
</span>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<span style="font-weight: bold; color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">${NOTIFICATION_DESCRIPTIONS[notificationSubscriptionType].detailed}.</span>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<span>Click
|
||||||
|
<a href='https://manifold.markets/notifications?tab=settings'>here</a>
|
||||||
|
to manage the rest of your notification settings.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<p></p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import React, { memo, ReactNode, useEffect, useState } from 'react'
|
import React, { memo, ReactNode, useEffect, useState } from 'react'
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import {
|
import { PrivateUser } from 'common/user'
|
||||||
notification_subscription_types,
|
|
||||||
notification_destination_types,
|
|
||||||
PrivateUser,
|
|
||||||
} from 'common/user'
|
|
||||||
import { updatePrivateUser } from 'web/lib/firebase/users'
|
import { updatePrivateUser } from 'web/lib/firebase/users'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import {
|
import {
|
||||||
|
@ -30,6 +26,11 @@ import {
|
||||||
usePersistentState,
|
usePersistentState,
|
||||||
} from 'web/hooks/use-persistent-state'
|
} from 'web/hooks/use-persistent-state'
|
||||||
import { safeLocalStorage } from 'web/lib/util/local'
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
|
import { NOTIFICATION_DESCRIPTIONS } from 'common/notification'
|
||||||
|
import {
|
||||||
|
notification_destination_types,
|
||||||
|
notification_preference,
|
||||||
|
} from 'common/user-notification-preferences'
|
||||||
|
|
||||||
export function NotificationSettings(props: {
|
export function NotificationSettings(props: {
|
||||||
navigateToSection: string | undefined
|
navigateToSection: string | undefined
|
||||||
|
@ -38,7 +39,7 @@ export function NotificationSettings(props: {
|
||||||
const { navigateToSection, privateUser } = props
|
const { navigateToSection, privateUser } = props
|
||||||
const [showWatchModal, setShowWatchModal] = useState(false)
|
const [showWatchModal, setShowWatchModal] = useState(false)
|
||||||
|
|
||||||
const emailsEnabled: Array<keyof notification_subscription_types> = [
|
const emailsEnabled: Array<notification_preference> = [
|
||||||
'all_comments_on_watched_markets',
|
'all_comments_on_watched_markets',
|
||||||
'all_replies_to_my_comments_on_watched_markets',
|
'all_replies_to_my_comments_on_watched_markets',
|
||||||
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
||||||
|
@ -74,7 +75,7 @@ export function NotificationSettings(props: {
|
||||||
// 'probability_updates_on_watched_markets',
|
// 'probability_updates_on_watched_markets',
|
||||||
// 'limit_order_fills',
|
// 'limit_order_fills',
|
||||||
]
|
]
|
||||||
const browserDisabled: Array<keyof notification_subscription_types> = [
|
const browserDisabled: Array<notification_preference> = [
|
||||||
'trending_markets',
|
'trending_markets',
|
||||||
'profit_loss_updates',
|
'profit_loss_updates',
|
||||||
'onboarding_flow',
|
'onboarding_flow',
|
||||||
|
@ -83,91 +84,82 @@ export function NotificationSettings(props: {
|
||||||
|
|
||||||
type SectionData = {
|
type SectionData = {
|
||||||
label: string
|
label: string
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: Partial<notification_preference>[]
|
||||||
[key in keyof Partial<notification_subscription_types>]: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const comments: SectionData = {
|
const comments: SectionData = {
|
||||||
label: 'New Comments',
|
label: 'New Comments',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
all_comments_on_watched_markets: 'All new comments',
|
'all_comments_on_watched_markets',
|
||||||
all_comments_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
|
'all_comments_on_contracts_with_shares_in_on_watched_markets',
|
||||||
// TODO: combine these two
|
// TODO: combine these two
|
||||||
all_replies_to_my_comments_on_watched_markets:
|
'all_replies_to_my_comments_on_watched_markets',
|
||||||
'Only replies to your comments',
|
'all_replies_to_my_answers_on_watched_markets',
|
||||||
all_replies_to_my_answers_on_watched_markets:
|
],
|
||||||
'Only replies to your answers',
|
|
||||||
// comments_by_followed_users_on_watched_markets: 'By followed users',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers: SectionData = {
|
const answers: SectionData = {
|
||||||
label: 'New Answers',
|
label: 'New Answers',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
all_answers_on_watched_markets: 'All new answers',
|
'all_answers_on_watched_markets',
|
||||||
all_answers_on_contracts_with_shares_in_on_watched_markets: `Only on markets you're invested in`,
|
'all_answers_on_contracts_with_shares_in_on_watched_markets',
|
||||||
// answers_by_followed_users_on_watched_markets: 'By followed users',
|
],
|
||||||
// answers_by_market_creator_on_watched_markets: 'By market creator',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const updates: SectionData = {
|
const updates: SectionData = {
|
||||||
label: 'Updates & Resolutions',
|
label: 'Updates & Resolutions',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
market_updates_on_watched_markets: 'All creator updates',
|
'market_updates_on_watched_markets',
|
||||||
market_updates_on_watched_markets_with_shares_in: `Only creator updates on markets you're invested in`,
|
'market_updates_on_watched_markets_with_shares_in',
|
||||||
resolutions_on_watched_markets: 'All market resolutions',
|
'resolutions_on_watched_markets',
|
||||||
resolutions_on_watched_markets_with_shares_in: `Only market resolutions you're invested in`,
|
'resolutions_on_watched_markets_with_shares_in',
|
||||||
// probability_updates_on_watched_markets: 'Probability updates',
|
],
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const yourMarkets: SectionData = {
|
const yourMarkets: SectionData = {
|
||||||
label: 'Markets You Created',
|
label: 'Markets You Created',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
your_contract_closed: 'Your market has closed (and needs resolution)',
|
'your_contract_closed',
|
||||||
all_comments_on_my_markets: 'Comments on your markets',
|
'all_comments_on_my_markets',
|
||||||
all_answers_on_my_markets: 'Answers on your markets',
|
'all_answers_on_my_markets',
|
||||||
subsidized_your_market: 'Your market was subsidized',
|
'subsidized_your_market',
|
||||||
tips_on_your_markets: 'Likes on your markets',
|
'tips_on_your_markets',
|
||||||
},
|
],
|
||||||
}
|
}
|
||||||
const bonuses: SectionData = {
|
const bonuses: SectionData = {
|
||||||
label: 'Bonuses',
|
label: 'Bonuses',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
betting_streaks: 'Prediction streak bonuses',
|
'betting_streaks',
|
||||||
referral_bonuses: 'Referral bonuses from referring users',
|
'referral_bonuses',
|
||||||
unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets',
|
'unique_bettors_on_your_contract',
|
||||||
},
|
],
|
||||||
}
|
}
|
||||||
const otherBalances: SectionData = {
|
const otherBalances: SectionData = {
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
loan_income: 'Automatic loans from your profitable bets',
|
'loan_income',
|
||||||
limit_order_fills: 'Limit order fills',
|
'limit_order_fills',
|
||||||
tips_on_your_comments: 'Tips on your comments',
|
'tips_on_your_comments',
|
||||||
},
|
],
|
||||||
}
|
}
|
||||||
const userInteractions: SectionData = {
|
const userInteractions: SectionData = {
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
tagged_user: 'A user tagged you',
|
'tagged_user',
|
||||||
on_new_follow: 'Someone followed you',
|
'on_new_follow',
|
||||||
contract_from_followed_user: 'New markets created by users you follow',
|
'contract_from_followed_user',
|
||||||
},
|
],
|
||||||
}
|
}
|
||||||
const generalOther: SectionData = {
|
const generalOther: SectionData = {
|
||||||
label: 'Other',
|
label: 'Other',
|
||||||
subscriptionTypeToDescription: {
|
subscriptionTypes: [
|
||||||
trending_markets: 'Weekly interesting markets',
|
'trending_markets',
|
||||||
thank_you_for_purchases: 'Thank you notes for your purchases',
|
'thank_you_for_purchases',
|
||||||
onboarding_flow: 'Explanatory emails to help you get started',
|
'onboarding_flow',
|
||||||
// profit_loss_updates: 'Weekly profit/loss updates',
|
],
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationSettingLine(props: {
|
function NotificationSettingLine(props: {
|
||||||
description: string
|
description: string
|
||||||
subscriptionTypeKey: keyof notification_subscription_types
|
subscriptionTypeKey: notification_preference
|
||||||
destinations: notification_destination_types[]
|
destinations: notification_destination_types[]
|
||||||
}) {
|
}) {
|
||||||
const { description, subscriptionTypeKey, destinations } = props
|
const { description, subscriptionTypeKey, destinations } = props
|
||||||
|
@ -237,9 +229,7 @@ export function NotificationSettings(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsersSavedPreference = (
|
const getUsersSavedPreference = (key: notification_preference) => {
|
||||||
key: keyof notification_subscription_types
|
|
||||||
) => {
|
|
||||||
return privateUser.notificationPreferences[key] ?? []
|
return privateUser.notificationPreferences[key] ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,17 +238,17 @@ export function NotificationSettings(props: {
|
||||||
data: SectionData
|
data: SectionData
|
||||||
}) {
|
}) {
|
||||||
const { icon, data } = props
|
const { icon, data } = props
|
||||||
const { label, subscriptionTypeToDescription } = data
|
const { label, subscriptionTypes } = data
|
||||||
const expand =
|
const expand =
|
||||||
navigateToSection &&
|
navigateToSection &&
|
||||||
Object.keys(subscriptionTypeToDescription).includes(navigateToSection)
|
Object.keys(subscriptionTypes).includes(navigateToSection)
|
||||||
|
|
||||||
// Not sure how to prevent re-render (and collapse of an open section)
|
// Not sure how to prevent re-render (and collapse of an open section)
|
||||||
// due to a private user settings change. Just going to persist expanded state here
|
// due to a private user settings change. Just going to persist expanded state here
|
||||||
const [expanded, setExpanded] = usePersistentState(expand ?? false, {
|
const [expanded, setExpanded] = usePersistentState(expand ?? false, {
|
||||||
key:
|
key:
|
||||||
'NotificationsSettingsSection-' +
|
'NotificationsSettingsSection-' +
|
||||||
Object.keys(subscriptionTypeToDescription).join('-'),
|
Object.keys(subscriptionTypes).join('-'),
|
||||||
store: storageStore(safeLocalStorage()),
|
store: storageStore(safeLocalStorage()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -287,13 +277,13 @@ export function NotificationSettings(props: {
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
|
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
|
||||||
{Object.entries(subscriptionTypeToDescription).map(([key, value]) => (
|
{subscriptionTypes.map((subType) => (
|
||||||
<NotificationSettingLine
|
<NotificationSettingLine
|
||||||
subscriptionTypeKey={key as keyof notification_subscription_types}
|
subscriptionTypeKey={subType as notification_preference}
|
||||||
destinations={getUsersSavedPreference(
|
destinations={getUsersSavedPreference(
|
||||||
key as keyof notification_subscription_types
|
subType as notification_preference
|
||||||
)}
|
)}
|
||||||
description={value}
|
description={NOTIFICATION_DESCRIPTIONS[subType].simple}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user