From e18ac28675257e51ca4c80e836fc1a4da574819b Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 8 Sep 2022 09:05:42 -0600 Subject: [PATCH] Notifications Settings page working --- common/user.ts | 40 ++ firestore.rules | 2 +- web/components/NotificationSettings.tsx | 236 ----------- ...arket-modal.tsx => watch-market-modal.tsx} | 11 +- web/components/follow-market-button.tsx | 4 +- web/components/notification-settings.tsx | 379 ++++++++++++++++++ web/pages/notifications.tsx | 2 +- 7 files changed, 429 insertions(+), 245 deletions(-) delete mode 100644 web/components/NotificationSettings.tsx rename web/components/contract/{follow-market-modal.tsx => watch-market-modal.tsx} (74%) create mode 100644 web/components/notification-settings.tsx diff --git a/common/user.ts b/common/user.ts index 0e333278..4c729389 100644 --- a/common/user.ts +++ b/common/user.ts @@ -64,8 +64,48 @@ export type PrivateUser = { initialIpAddress?: string apiKey?: string notificationPreferences?: notification_subscribe_types + notificationSubscriptionTypes: exhaustive_notification_subscribe_types } +export type notification_receive_types = 'email' | 'browser' + +export type exhaustive_notification_subscribe_types = { + // Watched Markets + all_comments: notification_receive_types[] // Email currently - seems bad + all_answers: notification_receive_types[] // Email currently - seems bad + + // Comments + tipped_comments: notification_receive_types[] // Email + comments_by_followed_users: notification_receive_types[] + all_replies_to_my_comments: notification_receive_types[] // Email + all_replies_to_my_answers: notification_receive_types[] // Email + + // Answers + answers_by_followed_users: notification_receive_types[] + answers_by_market_creator: notification_receive_types[] + + // On users' markets + my_markets_closed: notification_receive_types[] // Email, Recommended + all_comments_on_my_markets: notification_receive_types[] // Email + all_answers_on_my_markets: notification_receive_types[] // Email + + // Market updates + resolutions: notification_receive_types[] // Email + market_updates: notification_receive_types[] + probability_updates: notification_receive_types[] // Email - would want persistent changes only though + + // Balance Changes + loans: notification_receive_types[] + betting_streaks: notification_receive_types[] + referral_bonuses: notification_receive_types[] + unique_bettor_bonuses: notification_receive_types[] + + // General + user_tagged_you: notification_receive_types[] // Email + new_markets_by_followed_users: notification_receive_types[] // Email + trending_markets: notification_receive_types[] // Email + profit_loss_updates: notification_receive_types[] // Email +} export type notification_subscribe_types = 'all' | 'less' | 'none' export type PortfolioMetrics = { diff --git a/firestore.rules b/firestore.rules index 15b60d0f..eeee9b26 100644 --- a/firestore.rules +++ b/firestore.rules @@ -75,7 +75,7 @@ service cloud.firestore { allow read: if userId == request.auth.uid || isAdmin(); allow update: if (userId == request.auth.uid || isAdmin()) && request.resource.data.diff(resource.data).affectedKeys() - .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails' ]); + .hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences', 'unsubscribedFromWeeklyTrendingEmails','notificationSubscriptionTypes' ]); } match /private-users/{userId}/views/{viewId} { diff --git a/web/components/NotificationSettings.tsx b/web/components/NotificationSettings.tsx deleted file mode 100644 index 7ee27fb5..00000000 --- a/web/components/NotificationSettings.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { useUser } from 'web/hooks/use-user' -import React, { useEffect, useState } from 'react' -import { notification_subscribe_types, PrivateUser } from 'common/user' -import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' -import toast from 'react-hot-toast' -import { track } from '@amplitude/analytics-browser' -import { LoadingIndicator } from 'web/components/loading-indicator' -import { Row } from 'web/components/layout/row' -import clsx from 'clsx' -import { CheckIcon, XIcon } from '@heroicons/react/outline' -import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { Col } from 'web/components/layout/col' -import { FollowMarketModal } from 'web/components/contract/follow-market-modal' - -export function NotificationSettings() { - const user = useUser() - const [notificationSettings, setNotificationSettings] = - useState('all') - const [emailNotificationSettings, setEmailNotificationSettings] = - useState('all') - const [privateUser, setPrivateUser] = useState(null) - const [showModal, setShowModal] = useState(false) - - useEffect(() => { - if (user) listenForPrivateUser(user.id, setPrivateUser) - }, [user]) - - useEffect(() => { - if (!privateUser) return - if (privateUser.notificationPreferences) { - setNotificationSettings(privateUser.notificationPreferences) - } - if ( - privateUser.unsubscribedFromResolutionEmails && - privateUser.unsubscribedFromCommentEmails && - privateUser.unsubscribedFromAnswerEmails - ) { - setEmailNotificationSettings('none') - } else if ( - !privateUser.unsubscribedFromResolutionEmails && - !privateUser.unsubscribedFromCommentEmails && - !privateUser.unsubscribedFromAnswerEmails - ) { - setEmailNotificationSettings('all') - } else { - setEmailNotificationSettings('less') - } - }, [privateUser]) - - const loading = 'Changing Notifications Settings' - const success = 'Notification Settings Changed!' - function changeEmailNotifications(newValue: notification_subscribe_types) { - if (!privateUser) return - if (newValue === 'all') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: false, - unsubscribedFromCommentEmails: false, - unsubscribedFromAnswerEmails: false, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } else if (newValue === 'less') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: false, - unsubscribedFromCommentEmails: true, - unsubscribedFromAnswerEmails: true, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } else if (newValue === 'none') { - toast.promise( - updatePrivateUser(privateUser.id, { - unsubscribedFromResolutionEmails: true, - unsubscribedFromCommentEmails: true, - unsubscribedFromAnswerEmails: true, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } - } - - function changeInAppNotificationSettings( - newValue: notification_subscribe_types - ) { - if (!privateUser) return - track('In-App Notification Preferences Changed', { - newPreference: newValue, - oldPreference: privateUser.notificationPreferences, - }) - toast.promise( - updatePrivateUser(privateUser.id, { - notificationPreferences: newValue, - }), - { - loading, - success, - error: (err) => `${err.message}`, - } - ) - } - - useEffect(() => { - if (privateUser && privateUser.notificationPreferences) - setNotificationSettings(privateUser.notificationPreferences) - else setNotificationSettings('all') - }, [privateUser]) - - if (!privateUser) { - return - } - - function NotificationSettingLine(props: { - label: string | React.ReactNode - highlight: boolean - onClick?: () => void - }) { - const { label, highlight, onClick } = props - return ( - - {highlight ? : } - {label} - - ) - } - - return ( -
-
In App Notifications
- - changeInAppNotificationSettings( - choice as notification_subscribe_types - ) - } - className={'col-span-4 p-2'} - toggleClassName={'w-24'} - /> -
- - - You will receive notifications for these general events: - - - - You will receive new comment, answer, & resolution notifications on - questions: - - - That you watch - you - auto-watch questions if: - - } - onClick={() => setShowModal(true)} - /> - - • You create it - • You bet, comment on, or answer it - • You add liquidity to it - - • If you select 'Less' and you've commented on or answered a - question, you'll only receive notification on direct replies to - your comments or answers - - - -
-
Email Notifications
- - changeEmailNotifications(choice as notification_subscribe_types) - } - className={'col-span-4 p-2'} - toggleClassName={'w-24'} - /> -
-
- You will receive emails for: - - - - -
-
- -
- ) -} diff --git a/web/components/contract/follow-market-modal.tsx b/web/components/contract/watch-market-modal.tsx similarity index 74% rename from web/components/contract/follow-market-modal.tsx rename to web/components/contract/watch-market-modal.tsx index fb62ce9f..2fb9bc00 100644 --- a/web/components/contract/follow-market-modal.tsx +++ b/web/components/contract/watch-market-modal.tsx @@ -4,7 +4,7 @@ import { EyeIcon } from '@heroicons/react/outline' import React from 'react' import clsx from 'clsx' -export const FollowMarketModal = (props: { +export const WatchMarketModal = (props: { open: boolean setOpen: (b: boolean) => void title?: string @@ -18,20 +18,21 @@ export const FollowMarketModal = (props: { • What is watching? - You can receive notifications on questions you're interested in by + You'll receive notifications on markets by betting, commenting, or clicking the • What types of notifications will I receive? - You'll receive in-app notifications for new comments, answers, and - updates to the question. + You'll receive notifications for new comments, answers, and updates + to the question. See the notifications settings pages to customize + which types of notifications you receive on watched markets. diff --git a/web/components/follow-market-button.tsx b/web/components/follow-market-button.tsx index 332b044a..cddc6db4 100644 --- a/web/components/follow-market-button.tsx +++ b/web/components/follow-market-button.tsx @@ -11,7 +11,7 @@ import { User } from 'common/user' import { useContractFollows } from 'web/hooks/use-follows' import { firebaseLogin, updateUser } from 'web/lib/firebase/users' import { track } from 'web/lib/service/analytics' -import { FollowMarketModal } from 'web/components/contract/follow-market-modal' +import { WatchMarketModal } from 'web/components/contract/follow-market-modal' import { useState } from 'react' import { Col } from 'web/components/layout/col' @@ -65,7 +65,7 @@ export const FollowMarketButton = (props: { Watch )} - { + const browser = browserIf ? 'browser' : undefined + const email = emailIf ? 'email' : undefined + return filterDefined([browser, email]) as notification_receive_types[] + } + if (privateUser && !privateUser.notificationSubscriptionTypes) { + updatePrivateUser(privateUser.id, { + notificationSubscriptionTypes: { + // Watched Markets + all_comments: constructPref( + wantsAll, + !privateUser.unsubscribedFromCommentEmails + ), + all_answers: constructPref( + wantsAll, + !privateUser.unsubscribedFromAnswerEmails + ), + + // Comments + tipped_comments: constructPref(wantsAll || wantsLess, true), + comments_by_followed_users: constructPref(wantsAll, false), //wantsAll ? browserOnly : none, + all_replies_to_my_comments: constructPref(wantsAll || wantsLess, true), //wantsAll || wantsLess ? both : none, + all_replies_to_my_answers: constructPref(wantsAll || wantsLess, true), //wantsAll || wantsLess ? both : none, + + // Answers + answers_by_followed_users: constructPref( + wantsAll || wantsLess, + !privateUser.unsubscribedFromAnswerEmails + ), //wantsAll || wantsLess ? both : none, + answers_by_market_creator: constructPref( + wantsAll || wantsLess, + !privateUser.unsubscribedFromAnswerEmails + ), //wantsAll || wantsLess ? both : none, + + // On users' markets + my_markets_closed: constructPref( + wantsAll || wantsLess, + !privateUser.unsubscribedFromResolutionEmails + ), //wantsAll || wantsLess ? both : none, // High priority + all_comments_on_my_markets: constructPref(wantsAll || wantsLess, true), //wantsAll || wantsLess ? both : none, + all_answers_on_my_markets: constructPref(wantsAll || wantsLess, true), //wantsAll || wantsLess ? both : none, + + // Market updates + resolutions: constructPref(wantsAll || wantsLess, true), + market_updates: constructPref(wantsAll || wantsLess, false), + + //Balance Changes + loans: browserOnly, + betting_streaks: browserOnly, + referral_bonuses: both, + unique_bettor_bonuses: browserOnly, + + // General + user_tagged_you: constructPref(wantsAll || wantsLess, true), //wantsAll || wantsLess ? both : none, + new_markets_by_followed_users: constructPref( + wantsAll || wantsLess, + true + ), //wantsAll || wantsLess ? both : none, + trending_markets: constructPref( + false, + !privateUser.unsubscribedFromWeeklyTrendingEmails + ), + profit_loss_updates: emailOnly, + } as exhaustive_notification_subscribe_types, + }) + } + + if (!privateUser || !privateUser.notificationSubscriptionTypes) { + return + } + + const emailsEnabled = [ + 'all_comments', + 'all_answers', + 'resolutions', + 'all_replies_to_my_comments', + 'all_replies_to_my_answers', + 'all_comments_on_my_markets', + 'all_answers_on_my_markets', + 'my_markets_closed', + 'probability_updates', + 'user_tagged_you', + 'new_markets_by_followed_users', + 'trending_markets', + 'profit_loss_updates', + ] + const browserDisabled = ['trending_markets', 'profit_loss_updates'] + + const watched_markets_explanations_comments: { + [key in keyof Partial]: string + } = { + all_comments: 'All', + // tipped_comments: 'Tipped', + // comments_by_followed_users: 'By followed users', + all_replies_to_my_comments: 'Replies to your comments', + } + const watched_markets_explanations_answers: { + [key in keyof Partial]: string + } = { + all_answers: 'All', + all_replies_to_my_answers: 'Replies to your answers', + // answers_by_followed_users: 'By followed users', + // answers_by_market_creator: 'Submitted by the market creator', + } + const watched_markets_explanations_your_markets: { + [key in keyof Partial]: string + } = { + my_markets_closed: 'Your market has closed (and needs resolution)', + all_comments_on_my_markets: 'Comments on your markets', + all_answers_on_my_markets: 'Answers on your markets', + } + const watched_markets_explanations_market_updates: { + [key in keyof Partial]: string + } = { + resolutions: 'Market resolutions', + market_updates: 'Updates made by the creator', + // probability_updates: 'Changes in probability', + } + + const balance_change_explanations: { + [key in keyof Partial]: string + } = { + loans: 'Automatic loans from your profitable bets', + betting_streaks: 'Betting streak bonuses', + referral_bonuses: 'Referral bonuses from referring users', + unique_bettor_bonuses: 'Unique bettor bonuses on your markets', + } + + const general_explanations: { + [key in keyof Partial]: string + } = { + user_tagged_you: 'A user tagged you', + new_markets_by_followed_users: 'New markets created by users you follow', + trending_markets: 'Weekly trending markets', + // profit_loss_updates: 'Weekly profit and loss updates', + } + + const NotificationSettingLine = ( + description: string, + key: string, + value: notification_receive_types[] + ) => { + const previousInAppValue = value.includes('browser') + const previousEmailValue = value.includes('email') + const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue) + const [emailEnabled, setEmailEnabled] = useState(previousEmailValue) + const loading = 'Changing Notifications Settings' + const success = 'Changed Notification Settings!' + + useEffect(() => { + if ( + inAppEnabled !== previousInAppValue || + emailEnabled !== previousEmailValue + ) { + toast.promise( + updatePrivateUser(privateUser.id, { + notificationSubscriptionTypes: { + ...privateUser.notificationSubscriptionTypes, + [key]: filterDefined([ + inAppEnabled ? 'browser' : undefined, + emailEnabled ? 'email' : undefined, + ]), + }, + }), + { + success, + loading, + error: 'Error changing notification settings. Try again?', + } + ) + } + }, [ + inAppEnabled, + emailEnabled, + previousInAppValue, + previousEmailValue, + key, + ]) + + // for each entry in the exhaustive_notification_subscribe_types we'll want to load whether the user + // wants email, browser, both, or none + return ( + + + + {description} + + + {!browserDisabled.includes(key) && ( + + + + + + In-app + + + + )} + {emailsEnabled.includes(key) && ( + + + + + + Emails + + + + )} + + + + ) + } + + const getUsersSavedPreference = (key: string) => { + return Object.keys(privateUser.notificationSubscriptionTypes).includes(key) + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + privateUser.notificationSubscriptionTypes[ + Object.keys(privateUser.notificationSubscriptionTypes).filter( + (x) => x === key + )[0] + ] + : '' + } + + const Section = ( + icon: ReactNode, + label: string, + map: { [key: string]: string } + ) => { + const [expanded, setExpanded] = useState(false) + return ( + + setExpanded(!expanded)} + > + {icon} + {label} + + {expanded ? ( + + Hide + + ) : ( + + Show + + )} + + + {Object.entries(map).map(([key, value]) => + NotificationSettingLine(value, key, getUsersSavedPreference(key)) + )} + + + ) + } + + return ( +
+ + + Notifications for Watched Markets + setShowWatchModal(true)} + /> + + {/*// TODO: add none option to each section*/} + {Section( + , + 'New Comments', + watched_markets_explanations_comments + )} + {Section( + , + 'New Answers', + watched_markets_explanations_answers + )} + {Section( + , + 'On Your Markets', + watched_markets_explanations_your_markets + )} + {Section( + , + 'Market Updates', + watched_markets_explanations_market_updates + )} + + Balance Changes + + {Section( + , + 'Loans and Bonuses', + balance_change_explanations + )} + + Other + + {Section( + , + 'General', + general_explanations + )} + + +
+ ) +} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index ccfbf371..8d97a75b 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -40,7 +40,7 @@ import { Pagination } from 'web/components/pagination' import { useWindowSize } from 'web/hooks/use-window-size' import { safeLocalStorage } from 'web/lib/util/local' import { SiteLink } from 'web/components/site-link' -import { NotificationSettings } from 'web/components/NotificationSettings' +import { NotificationSettings } from 'web/components/notification-settings' import { SEO } from 'web/components/SEO' import { usePrivateUser, useUser } from 'web/hooks/use-user' import { UserLink } from 'web/components/user-link'