manifold/web/components/notification-settings.tsx
2022-10-10 14:41:24 -06:00

391 lines
12 KiB
TypeScript

import React, { memo, ReactNode, useEffect, useState } from 'react'
import { Row } from 'web/components/layout/row'
import clsx from 'clsx'
import { PrivateUser } from 'common/user'
import { updatePrivateUser } from 'web/lib/firebase/users'
import { Col } from 'web/components/layout/col'
import {
CashIcon,
ChatIcon,
ChevronDownIcon,
ChevronUpIcon,
CurrencyDollarIcon,
ExclamationIcon,
InboxInIcon,
InformationCircleIcon,
LightBulbIcon,
TrendingUpIcon,
UserIcon,
UsersIcon,
} from '@heroicons/react/outline'
import { WatchMarketModal } from 'web/components/contract/watch-market-modal'
import toast from 'react-hot-toast'
import { SwitchSetting } from 'web/components/switch-setting'
import { uniq } from 'lodash'
import {
storageStore,
usePersistentState,
} from 'web/hooks/use-persistent-state'
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: {
navigateToSection: string | undefined
privateUser: PrivateUser
}) {
const { navigateToSection, privateUser } = props
const [showWatchModal, setShowWatchModal] = useState(false)
const emailsEnabled: Array<notification_preference> = [
'all_comments_on_watched_markets',
'all_replies_to_my_comments_on_watched_markets',
'all_comments_on_contracts_with_shares_in_on_watched_markets',
'all_answers_on_watched_markets',
'all_replies_to_my_answers_on_watched_markets',
'all_answers_on_contracts_with_shares_in_on_watched_markets',
'your_contract_closed',
'all_comments_on_my_markets',
'all_answers_on_my_markets',
'resolutions_on_watched_markets_with_shares_in',
'resolutions_on_watched_markets',
'trending_markets',
'onboarding_flow',
'thank_you_for_purchases',
'tagged_user', // missing tagged on contract description email
'contract_from_followed_user',
'unique_bettors_on_your_contract',
'profit_loss_updates',
'opt_out_all',
// TODO: add these
// biggest winner, here are the rest of your markets
// 'referral_bonuses',
// 'on_new_follow',
// 'tips_on_your_markets',
// 'tips_on_your_comments',
// maybe the following?
// 'probability_updates_on_watched_markets',
// 'limit_order_fills',
]
const browserDisabled: Array<notification_preference> = [
'trending_markets',
'profit_loss_updates',
'onboarding_flow',
'thank_you_for_purchases',
]
type SectionData = {
label: string
subscriptionTypes: Partial<notification_preference>[]
}
const comments: SectionData = {
label: 'New Comments',
subscriptionTypes: [
'all_comments_on_watched_markets',
'all_comments_on_contracts_with_shares_in_on_watched_markets',
// TODO: combine these two
'all_replies_to_my_comments_on_watched_markets',
'all_replies_to_my_answers_on_watched_markets',
],
}
const answers: SectionData = {
label: 'New Answers',
subscriptionTypes: [
'all_answers_on_watched_markets',
'all_answers_on_contracts_with_shares_in_on_watched_markets',
],
}
const updates: SectionData = {
label: 'Updates & Resolutions',
subscriptionTypes: [
'market_updates_on_watched_markets',
'market_updates_on_watched_markets_with_shares_in',
'resolutions_on_watched_markets',
'resolutions_on_watched_markets_with_shares_in',
],
}
const yourMarkets: SectionData = {
label: 'Markets You Created',
subscriptionTypes: [
// 'your_contract_closed',
'all_comments_on_my_markets',
'all_answers_on_my_markets',
'subsidized_your_market',
'tips_on_your_markets',
],
}
const bonuses: SectionData = {
label: 'Bonuses',
subscriptionTypes: [
'betting_streaks',
'referral_bonuses',
'unique_bettors_on_your_contract',
],
}
const otherBalances: SectionData = {
label: 'Other',
subscriptionTypes: [
'loan_income',
'limit_order_fills',
'tips_on_your_comments',
'badges_awarded',
],
}
const userInteractions: SectionData = {
label: 'Users',
subscriptionTypes: [
'tagged_user',
'on_new_follow',
'contract_from_followed_user',
],
}
const generalOther: SectionData = {
label: 'Other',
subscriptionTypes: [
'trending_markets',
'thank_you_for_purchases',
'onboarding_flow',
'profit_loss_updates',
],
}
const optOut: SectionData = {
label: 'Opt Out',
subscriptionTypes: ['opt_out_all'],
}
function NotificationSettingLine(props: {
description: string
subscriptionTypeKey: notification_preference
destinations: notification_destination_types[]
optOutAll: notification_destination_types[]
}) {
const { description, subscriptionTypeKey, destinations, optOutAll } = props
const previousInAppValue = destinations.includes('browser')
const previousEmailValue = destinations.includes('email')
const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue)
const [emailEnabled, setEmailEnabled] = useState(previousEmailValue)
const [error, setError] = useState<string>('')
const loading = 'Changing Notifications Settings'
const success = 'Changed Notification Settings!'
const highlight = navigateToSection === subscriptionTypeKey
const attemptToChangeSetting = (
setting: 'browser' | 'email',
newValue: boolean
) => {
const necessaryError =
'This notification type is necessary. At least one destination must be enabled.'
const necessarySetting =
NOTIFICATION_DESCRIPTIONS[subscriptionTypeKey].necessary
if (
necessarySetting &&
setting === 'browser' &&
!emailEnabled &&
!newValue
) {
setError(necessaryError)
return
} else if (
necessarySetting &&
setting === 'email' &&
!inAppEnabled &&
!newValue
) {
setError(necessaryError)
return
}
changeSetting(setting, newValue)
}
const changeSetting = (setting: 'browser' | 'email', newValue: boolean) => {
toast
.promise(
updatePrivateUser(privateUser.id, {
notificationPreferences: {
...privateUser.notificationPreferences,
[subscriptionTypeKey]: destinations.includes(setting)
? destinations.filter((d) => d !== setting)
: uniq([...destinations, setting]),
},
}),
{
success,
loading,
error: 'Error changing notification settings. Try again?',
}
)
.then(() => {
if (setting === 'browser') {
setInAppEnabled(newValue)
} else {
setEmailEnabled(newValue)
}
})
}
return (
<Row
className={clsx(
'my-1 gap-1 text-gray-300',
highlight ? 'rounded-md bg-indigo-100 p-1' : ''
)}
>
<Col className="ml-3 gap-2 text-sm">
<Row className="gap-2 font-medium text-gray-700">
<span>{description}</span>
</Row>
<Row className={'gap-4'}>
{!browserDisabled.includes(subscriptionTypeKey) && (
<SwitchSetting
checked={inAppEnabled}
onChange={(newVal) => attemptToChangeSetting('browser', newVal)}
label={'Web'}
disabled={optOutAll.includes('browser')}
/>
)}
{emailsEnabled.includes(subscriptionTypeKey) && (
<SwitchSetting
checked={emailEnabled}
onChange={(newVal) => attemptToChangeSetting('email', newVal)}
label={'Email'}
disabled={optOutAll.includes('email')}
/>
)}
</Row>
{error && <span className={'text-error'}>{error}</span>}
</Col>
</Row>
)
}
const getUsersSavedPreference = (key: notification_preference) => {
return privateUser.notificationPreferences[key] ?? []
}
const Section = memo(function Section(props: {
icon: ReactNode
data: SectionData
}) {
const { icon, data } = props
const { label, subscriptionTypes } = data
const expand =
navigateToSection &&
subscriptionTypes.includes(navigateToSection as notification_preference)
// 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
const [expanded, setExpanded] = usePersistentState(expand ?? false, {
key: 'NotificationsSettingsSection-' + subscriptionTypes.join('-'),
store: storageStore(safeLocalStorage()),
})
// Not working as the default value for expanded, so using a useEffect
useEffect(() => {
if (expand) setExpanded(true)
}, [expand, setExpanded])
return (
<Col className={clsx('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')}>
{subscriptionTypes.map((subType) => (
<NotificationSettingLine
key={subType}
subscriptionTypeKey={subType as notification_preference}
destinations={getUsersSavedPreference(
subType as notification_preference
)}
description={NOTIFICATION_DESCRIPTIONS[subType].simple}
optOutAll={
subType === 'opt_out_all' || subType === 'your_contract_closed'
? []
: getUsersSavedPreference('opt_out_all')
}
/>
))}
</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>
<Section icon={<ChatIcon className={'h-6 w-6'} />} data={comments} />
<Section
icon={<TrendingUpIcon className={'h-6 w-6'} />}
data={updates}
/>
<Section
icon={<LightBulbIcon className={'h-6 w-6'} />}
data={answers}
/>
<Section icon={<UserIcon className={'h-6 w-6'} />} data={yourMarkets} />
<Row className={'gap-2 text-xl text-gray-700'}>
<span>Balance Changes</span>
</Row>
<Section
icon={<CurrencyDollarIcon className={'h-6 w-6'} />}
data={bonuses}
/>
<Section
icon={<CashIcon className={'h-6 w-6'} />}
data={otherBalances}
/>
<Row className={'gap-2 text-xl text-gray-700'}>
<span>General</span>
</Row>
<Section
icon={<UsersIcon className={'h-6 w-6'} />}
data={userInteractions}
/>
<Section
icon={<InboxInIcon className={'h-6 w-6'} />}
data={generalOther}
/>
<Section
icon={<ExclamationIcon className={'h-6 w-6'} />}
data={optOut}
/>
<WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} />
</Col>
</div>
)
}