Notifications Settings page working

This commit is contained in:
Ian Philips 2022-09-08 09:05:42 -06:00
parent 87060488f5
commit e18ac28675
7 changed files with 429 additions and 245 deletions

View File

@ -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 = {

View File

@ -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} {

View File

@ -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<notification_subscribe_types>('all')
const [emailNotificationSettings, setEmailNotificationSettings] =
useState<notification_subscribe_types>('all')
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(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 <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
}
function NotificationSettingLine(props: {
label: string | React.ReactNode
highlight: boolean
onClick?: () => void
}) {
const { label, highlight, onClick } = props
return (
<Row
className={clsx(
'my-1 gap-1 text-gray-300',
highlight && '!text-black',
onClick ? 'cursor-pointer' : ''
)}
onClick={onClick}
>
{highlight ? <CheckIcon height={20} /> : <XIcon height={20} />}
{label}
</Row>
)
}
return (
<div className={'p-2'}>
<div>In App Notifications</div>
<ChoicesToggleGroup
currentChoice={notificationSettings}
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
setChoice={(choice) =>
changeInAppNotificationSettings(
choice as notification_subscribe_types
)
}
className={'col-span-4 p-2'}
toggleClassName={'w-24'}
/>
<div className={'mt-4 text-sm'}>
<Col className={''}>
<Row className={'my-1'}>
You will receive notifications for these general events:
</Row>
<NotificationSettingLine
highlight={notificationSettings !== 'none'}
label={"Income & referral bonuses you've received"}
/>
<Row className={'my-1'}>
You will receive new comment, answer, & resolution notifications on
questions:
</Row>
<NotificationSettingLine
highlight={notificationSettings !== 'none'}
label={
<span>
That <span className={'font-bold'}>you watch </span>- you
auto-watch questions if:
</span>
}
onClick={() => setShowModal(true)}
/>
<Col
className={clsx(
'mb-2 ml-8',
'gap-1 text-gray-300',
notificationSettings !== 'none' && '!text-black'
)}
>
<Row> You create it</Row>
<Row> You bet, comment on, or answer it</Row>
<Row> You add liquidity to it</Row>
<Row>
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
</Row>
</Col>
</Col>
</div>
<div className={'mt-4'}>Email Notifications</div>
<ChoicesToggleGroup
currentChoice={emailNotificationSettings}
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
setChoice={(choice) =>
changeEmailNotifications(choice as notification_subscribe_types)
}
className={'col-span-4 p-2'}
toggleClassName={'w-24'}
/>
<div className={'mt-4 text-sm'}>
<div>
You will receive emails for:
<NotificationSettingLine
label={"Resolution of questions you're betting on"}
highlight={emailNotificationSettings !== 'none'}
/>
<NotificationSettingLine
label={'Closure of your questions'}
highlight={emailNotificationSettings !== 'none'}
/>
<NotificationSettingLine
label={'Activity on your questions'}
highlight={emailNotificationSettings === 'all'}
/>
<NotificationSettingLine
label={"Activity on questions you've answered or commented on"}
highlight={emailNotificationSettings === 'all'}
/>
</div>
</div>
<FollowMarketModal setOpen={setShowModal} open={showModal} />
</div>
)
}

View File

@ -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: {
<Col className={'gap-2'}>
<span className={'text-indigo-700'}> What is watching?</span>
<span className={'ml-2'}>
You can receive notifications on questions you're interested in by
You'll receive notifications on markets by betting, commenting, or
clicking the
<EyeIcon
className={clsx('ml-1 inline h-6 w-6 align-top')}
aria-hidden="true"
/>
button on a question.
button on them.
</span>
<span className={'text-indigo-700'}>
What types of notifications will I receive?
</span>
<span className={'ml-2'}>
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.
</span>
</Col>
</Col>

View File

@ -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
</Col>
)}
<FollowMarketModal
<WatchMarketModal
open={open}
setOpen={setOpen}
title={`You ${

View File

@ -0,0 +1,379 @@
import { usePrivateUser } from 'web/hooks/use-user'
import React, { ReactNode, useEffect, useState } from 'react'
import { LoadingIndicator } from 'web/components/loading-indicator'
import { Row } from 'web/components/layout/row'
import clsx from 'clsx'
import {
exhaustive_notification_subscribe_types,
notification_receive_types,
} from 'common/user'
import { updatePrivateUser } from 'web/lib/firebase/users'
import { Switch } from '@headlessui/react'
import { Col } from 'web/components/layout/col'
import {
AdjustmentsIcon,
CashIcon,
ChatIcon,
ChevronDownIcon,
ChevronUpIcon,
InformationCircleIcon,
LightBulbIcon,
TrendingUpIcon,
UserIcon,
} from '@heroicons/react/outline'
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
import { filterDefined } from 'common/util/array'
import toast from 'react-hot-toast'
export function NotificationSettings() {
const privateUser = usePrivateUser()
const [showWatchModal, setShowWatchModal] = useState(false)
const prevPref = privateUser?.notificationPreferences
const browserOnly = ['browser']
const emailOnly = ['email']
const both = ['email', 'browser']
const wantsLess = prevPref === 'less'
const wantsAll = prevPref === 'all'
const constructPref = (browserIf: boolean, emailIf: boolean | undefined) => {
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 <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
}
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<exhaustive_notification_subscribe_types>]: 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<exhaustive_notification_subscribe_types>]: 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<exhaustive_notification_subscribe_types>]: 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<exhaustive_notification_subscribe_types>]: string
} = {
resolutions: 'Market resolutions',
market_updates: 'Updates made by the creator',
// probability_updates: 'Changes in probability',
}
const balance_change_explanations: {
[key in keyof Partial<exhaustive_notification_subscribe_types>]: 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<exhaustive_notification_subscribe_types>]: 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 (
<Row className={clsx('my-1 gap-1 text-gray-300')}>
<Col className="ml-3 gap-2 text-sm">
<Row className="gap-2 font-medium text-gray-700">
<span>{description}</span>
</Row>
<Row className={'gap-4'}>
{!browserDisabled.includes(key) && (
<Switch.Group as="div" className="flex items-center">
<Switch
checked={inAppEnabled}
onChange={setInAppEnabled}
className={clsx(
inAppEnabled ? 'bg-indigo-600' : 'bg-gray-200',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2'
)}
>
<span
aria-hidden="true"
className={clsx(
inAppEnabled ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm font-medium text-gray-900">
In-app
</span>
</Switch.Label>
</Switch.Group>
)}
{emailsEnabled.includes(key) && (
<Switch.Group as="div" className="flex items-center">
<Switch
checked={emailEnabled}
onChange={setEmailEnabled}
className={clsx(
emailEnabled ? 'bg-indigo-600' : 'bg-gray-200',
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2'
)}
>
<span
aria-hidden="true"
className={clsx(
emailEnabled ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span className="text-sm font-medium text-gray-900">
Emails
</span>
</Switch.Label>
</Switch.Group>
)}
</Row>
</Col>
</Row>
)
}
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 (
<Col className={'ml-2 gap-2'}>
<Row
className={'mt-1 cursor-pointer items-center gap-2 text-gray-600'}
onClick={() => setExpanded(!expanded)}
>
{icon}
<span>{label}</span>
{expanded ? (
<ChevronUpIcon className="h-5 w-5 text-xs text-gray-500">
Hide
</ChevronUpIcon>
) : (
<ChevronDownIcon className="h-5 w-5 text-xs text-gray-500">
Show
</ChevronDownIcon>
)}
</Row>
<Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}>
{Object.entries(map).map(([key, value]) =>
NotificationSettingLine(value, key, getUsersSavedPreference(key))
)}
</Col>
</Col>
)
}
return (
<div className={'p-2'}>
<Col className={'gap-6'}>
<Row className={'gap-2 text-xl text-gray-700'}>
<span>Notifications for Watched Markets</span>
<InformationCircleIcon
className="-mb-1 h-5 w-5 cursor-pointer text-gray-500"
onClick={() => setShowWatchModal(true)}
/>
</Row>
{/*// TODO: add none option to each section*/}
{Section(
<ChatIcon className={'h-6 w-6'} />,
'New Comments',
watched_markets_explanations_comments
)}
{Section(
<LightBulbIcon className={'h-6 w-6'} />,
'New Answers',
watched_markets_explanations_answers
)}
{Section(
<UserIcon className={'h-6 w-6'} />,
'On Your Markets',
watched_markets_explanations_your_markets
)}
{Section(
<TrendingUpIcon className={'h-6 w-6'} />,
'Market Updates',
watched_markets_explanations_market_updates
)}
<Row className={'gap-2 text-xl text-gray-700'}>
<span>Balance Changes</span>
</Row>
{Section(
<CashIcon className={'h-6 w-6'} />,
'Loans and Bonuses',
balance_change_explanations
)}
<Row className={'gap-2 text-xl text-gray-700'}>
<span>Other</span>
</Row>
{Section(
<AdjustmentsIcon className={'h-6 w-6'} />,
'General',
general_explanations
)}
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
</Col>
</div>
)
}

View File

@ -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'