From c4a012c4d043826850b70b818bd86e9440bbbf2d Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Wed, 8 Apr 2026 11:02:10 +0200 Subject: [PATCH] Add notification preferences for platform updates and improve UI for notification settings --- common/src/user-notification-preferences.ts | 7 +- web/components/layout/tabs.tsx | 8 ++ web/components/notifications.tsx | 126 ++++++++++++-------- web/components/switch-setting.tsx | 18 +-- web/components/widgets/short-toggle.tsx | 4 +- web/pages/notifications.tsx | 1 + 6 files changed, 105 insertions(+), 59 deletions(-) diff --git a/common/src/user-notification-preferences.ts b/common/src/user-notification-preferences.ts index 24fad52c..7a730c7a 100644 --- a/common/src/user-notification-preferences.ts +++ b/common/src/user-notification-preferences.ts @@ -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, diff --git a/web/components/layout/tabs.tsx b/web/components/layout/tabs.tsx index 6f4fe957..32917be9 100644 --- a/web/components/layout/tabs.tsx +++ b/web/components/layout/tabs.tsx @@ -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 ( type !== 'mobile', +) + export const NotificationSettings = () => ( {(user) => } ) @@ -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 ( -
-
-

+ +

+ + {SHOWN_NOTIFICATION_DESTINATION_TYPES.map((destinationType) => ( + + {t( + `notifications.options.${destinationType}`, + destinationType === 'email' ? 'By email' : 'In the app', + )} + + ))} + +

{t('notifications.heading', 'Where do you want to be notified when someone')}

{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} /> ))}
-
+ ) } @@ -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 - 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 ( -
+
{question}
- -
+ + {SHOWN_NOTIFICATION_DESTINATION_TYPES.map((destinationType) => ( + setValue(checked, destinationType)} + disabled={optOut.includes(destinationType) && type !== 'opt_out_all'} + /> + ))} + + ) } diff --git a/web/components/switch-setting.tsx b/web/components/switch-setting.tsx index 36c8ea2c..db50ab39 100644 --- a/web/components/switch-setting.tsx +++ b/web/components/switch-setting.tsx @@ -15,14 +15,16 @@ export const SwitchSetting = (props: { return ( - - {label} - + {label && ( + + {label} + + )} ) } diff --git a/web/components/widgets/short-toggle.tsx b/web/components/widgets/short-toggle.tsx index aa6f3a63..63ed1653 100644 --- a/web/components/widgets/short-toggle.tsx +++ b/web/components/widgets/short-toggle.tsx @@ -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' diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 0c1580d4..344dfdd9 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -19,6 +19,7 @@ import {useT} from 'web/lib/locale' export default function NotificationsPage() { useRedirectIfSignedOut() const t = useT() + return (