mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-10 17:48:41 -04:00
Add notification preferences for platform updates and improve UI for notification settings
This commit is contained in:
@@ -3,7 +3,8 @@ import {DOMAIN} from 'common/envs/constants'
|
||||
import {PrivateUser} from './user'
|
||||
import {filterDefined} from './util/array'
|
||||
|
||||
export type notification_destination_types = 'email' | 'browser' | 'mobile'
|
||||
export const NOTIFICATION_DESTINATION_TYPES = ['email', 'browser', 'mobile'] as const
|
||||
export type notification_destination_types = (typeof NOTIFICATION_DESTINATION_TYPES)[number]
|
||||
export type notification_preference = keyof notification_preferences
|
||||
export type notification_preferences = {
|
||||
new_match: notification_destination_types[]
|
||||
@@ -21,6 +22,7 @@ export type notification_preferences = {
|
||||
// General
|
||||
onboarding_flow: notification_destination_types[] // unused
|
||||
thank_you_for_purchases: notification_destination_types[] // unused
|
||||
platform_updates: notification_destination_types[]
|
||||
opt_out_all: notification_destination_types[]
|
||||
}
|
||||
|
||||
@@ -47,13 +49,14 @@ export const getDefaultNotificationPreferences = (isDev?: boolean) => {
|
||||
// General
|
||||
thank_you_for_purchases: constructPref(false, false, false),
|
||||
onboarding_flow: constructPref(true, true, false),
|
||||
platform_updates: constructPref(true, true, false),
|
||||
|
||||
opt_out_all: [],
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const UNSUBSCRIBE_URL = `https://${DOMAIN}/notifications`
|
||||
export const UNSUBSCRIBE_URL = `https://${DOMAIN}/notifications#1`
|
||||
export const getNotificationDestinationsForUser = (
|
||||
privateUser: PrivateUser,
|
||||
type: notification_preference,
|
||||
|
||||
@@ -173,6 +173,14 @@ export function UncontrolledTabs(props: TabProps & {defaultIndex?: number}) {
|
||||
if ((defaultIndex ?? 0) > props.tabs.length - 1) {
|
||||
console.error('default index greater than tabs length')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && /^#\d+$/.test(window.location.hash)) {
|
||||
const i = Number(window.location.hash.slice(1))
|
||||
if (i < props.tabs.length) setActiveIndex(i)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ControlledTabs
|
||||
{...rest}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import {PrivateUser} from 'common/user'
|
||||
import {
|
||||
NOTIFICATION_DESTINATION_TYPES,
|
||||
notification_destination_types,
|
||||
notification_preference,
|
||||
notification_preferences,
|
||||
} from 'common/user-notification-preferences'
|
||||
import {debounce} from 'lodash'
|
||||
import {useCallback} from 'react'
|
||||
import {MultiSelectAnswers} from 'web/components/answers/answer-compatibility-question-content'
|
||||
import {Row} from 'web/components/layout/row'
|
||||
import {SwitchSetting} from 'web/components/switch-setting'
|
||||
import {WithPrivateUser} from 'web/components/user/with-user'
|
||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useT} from 'web/lib/locale'
|
||||
|
||||
// Right now, "email" means emails and "browser" means in-app notifications + push notifs
|
||||
// TODO: figure out what to do with "mobile"
|
||||
const SHOWN_NOTIFICATION_DESTINATION_TYPES = NOTIFICATION_DESTINATION_TYPES.filter(
|
||||
(type) => type !== 'mobile',
|
||||
)
|
||||
|
||||
export const NotificationSettings = () => (
|
||||
<WithPrivateUser>{(user) => <LoadedNotificationSettings privateUser={user} />}</WithPrivateUser>
|
||||
)
|
||||
@@ -29,51 +37,68 @@ function LoadedNotificationSettings(props: {privateUser: PrivateUser}) {
|
||||
type: notification_preference
|
||||
question: string
|
||||
}[] = [
|
||||
{
|
||||
type: 'new_match',
|
||||
question: t('notifications.question.new_match', '... matches with you?'),
|
||||
},
|
||||
{
|
||||
type: 'new_message',
|
||||
question: t('notifications.question.new_message', '... sends you a new message?'),
|
||||
},
|
||||
// {
|
||||
// type: 'new_profile_like',
|
||||
// question: '... likes your profile?',
|
||||
// },
|
||||
{
|
||||
type: 'new_match',
|
||||
question: t(
|
||||
'notifications.question.new_match',
|
||||
'... matches with you (private interest signals)?',
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'new_endorsement',
|
||||
question: t('notifications.question.new_endorsement', '... endorses you?'),
|
||||
},
|
||||
// {
|
||||
// type: 'new_profile_ship',
|
||||
// question: '... ships you?',
|
||||
// },
|
||||
{
|
||||
type: 'tagged_user',
|
||||
question: t('notifications.question.tagged_user', '... mentions you?'),
|
||||
},
|
||||
// {
|
||||
// type: 'on_new_follow',
|
||||
// question: '... follows you?',
|
||||
// },
|
||||
{
|
||||
type: 'new_search_alerts',
|
||||
question: t('notifications.question.new_search_alerts', 'Alerts from bookmarked searches?'),
|
||||
},
|
||||
{
|
||||
type: 'opt_out_all',
|
||||
type: 'platform_updates',
|
||||
question: t(
|
||||
'notifications.question.opt_out_all',
|
||||
'Opt out of all notifications? (You can always change this later)',
|
||||
'notifications.question.platform_updates',
|
||||
'Platform updates (share, growth, new features, etc.)?',
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'opt_out_all',
|
||||
question: t('notifications.question.opt_out_all', 'Opt out of all notifications?'),
|
||||
},
|
||||
// {
|
||||
// type: 'new_profile_like',
|
||||
// question: '... likes your profile?',
|
||||
// },
|
||||
// {
|
||||
// type: 'new_profile_ship',
|
||||
// question: '... ships you?',
|
||||
// },
|
||||
// {
|
||||
// type: 'on_new_follow',
|
||||
// question: '... follows you?',
|
||||
// },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="flex flex-col gap-8 p-4">
|
||||
<p className="text-ink-700 font-medium">
|
||||
<Row className="mx-auto">
|
||||
<div className="flex flex-col gap-8 p-2">
|
||||
<Row className="justify-end gap-2">
|
||||
{SHOWN_NOTIFICATION_DESTINATION_TYPES.map((destinationType) => (
|
||||
<span className="text-ink-600 max-w-12 w-fit">
|
||||
{t(
|
||||
`notifications.options.${destinationType}`,
|
||||
destinationType === 'email' ? 'By email' : 'In the app',
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</Row>
|
||||
<p className="text-ink-700 font-medium pr-32">
|
||||
{t('notifications.heading', 'Where do you want to be notified when someone')}
|
||||
</p>
|
||||
{notificationTypes.map(({type, question}) => (
|
||||
@@ -85,10 +110,11 @@ function LoadedNotificationSettings(props: {privateUser: PrivateUser}) {
|
||||
onUpdate={(selected) => {
|
||||
setPrefs((prevPrefs) => ({...prevPrefs, [type]: selected}))
|
||||
}}
|
||||
optOut={prefs.opt_out_all}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -97,24 +123,27 @@ const NotificationOption = (props: {
|
||||
question: string
|
||||
selected: notification_destination_types[]
|
||||
onUpdate: (selected: notification_destination_types[]) => void
|
||||
optOut: notification_destination_types[]
|
||||
}) => {
|
||||
const {type, question, selected, onUpdate} = props
|
||||
const t = useT()
|
||||
const {type, question, selected, onUpdate, optOut} = props
|
||||
|
||||
const getSelectedValues = (destinations: string[]) => {
|
||||
const values: number[] = []
|
||||
if ((destinations ?? []).includes('email')) values.push(0)
|
||||
if ((destinations ?? []).includes('browser')) values.push(1)
|
||||
return values
|
||||
}
|
||||
const selectedValues = {
|
||||
email: selected.includes('email'),
|
||||
browser: selected.includes('browser'),
|
||||
} as Record<notification_destination_types, boolean>
|
||||
|
||||
const setValue = async (value: number[]) => {
|
||||
const newDestinations: notification_destination_types[] = []
|
||||
if (value.includes(0)) newDestinations.push('email')
|
||||
if (value.includes(1)) newDestinations.push('browser')
|
||||
const setValue = async (checked: boolean, destinationType: notification_destination_types) => {
|
||||
const newDestinations = new Set(selected)
|
||||
if (checked) {
|
||||
newDestinations.add(destinationType)
|
||||
} else {
|
||||
newDestinations.delete(destinationType)
|
||||
}
|
||||
|
||||
onUpdate(newDestinations)
|
||||
save(selected, newDestinations)
|
||||
const result = Array.from(newDestinations)
|
||||
|
||||
onUpdate(result)
|
||||
save(selected, result)
|
||||
}
|
||||
|
||||
const save = useCallback(
|
||||
@@ -143,16 +172,17 @@ const NotificationOption = (props: {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Row className="gap-2 w-full justify-between">
|
||||
<div className="text-ink-700 font-medium">{question}</div>
|
||||
<MultiSelectAnswers
|
||||
options={[
|
||||
t('notifications.options.email', 'By email'),
|
||||
t('notifications.options.page', 'On notifications page'),
|
||||
]}
|
||||
values={getSelectedValues(selected)}
|
||||
setValue={setValue}
|
||||
/>
|
||||
</div>
|
||||
<Row className="gap-3">
|
||||
{SHOWN_NOTIFICATION_DESTINATION_TYPES.map((destinationType) => (
|
||||
<SwitchSetting
|
||||
checked={selectedValues[destinationType]}
|
||||
onChange={(checked) => setValue(checked, destinationType)}
|
||||
disabled={optOut.includes(destinationType) && type !== 'opt_out_all'}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,14 +15,16 @@ export const SwitchSetting = (props: {
|
||||
return (
|
||||
<Switch.Group as="div" className="flex items-center gap-3" data-testid={testId}>
|
||||
<ShortToggle colorMode={colorMode} on={checked} setOn={onChange} disabled={disabled} />
|
||||
<Switch.Label
|
||||
className={clsx(
|
||||
'text-ink-900 text-sm',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{label && (
|
||||
<Switch.Label
|
||||
className={clsx(
|
||||
'text-ink-900 text-sm',
|
||||
disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
)}
|
||||
</Switch.Group>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ export default function ShortToggle(props: {
|
||||
'ring-amber-500 ring-offset-canvas-50 bg-amber-500': on && colorMode === 'warning',
|
||||
'bg-ink-300': !on,
|
||||
})
|
||||
const toggleEnabledClasses = !disabled ? 'cursor-pointer' : 'cursor-not-allowed opacity-50'
|
||||
const toggleEnabledClasses = !disabled
|
||||
? 'cursor-pointer hover:opacity-80'
|
||||
: 'cursor-not-allowed opacity-30'
|
||||
|
||||
const knobBaseClasses =
|
||||
'bg-canvas-0 pointer-events-none inline-block rounded-full ring-0 transition duration-200 ease-in-out'
|
||||
|
||||
@@ -19,6 +19,7 @@ import {useT} from 'web/lib/locale'
|
||||
export default function NotificationsPage() {
|
||||
useRedirectIfSignedOut()
|
||||
const t = useT()
|
||||
|
||||
return (
|
||||
<PageBase trackPageView={'notifications page'} className={'mx-4'}>
|
||||
<NoSEO />
|
||||
|
||||
Reference in New Issue
Block a user