Activity notification settings (#8485)

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: hailey <me@haileyok.com>
This commit is contained in:
Samuel Newman
2025-07-02 00:36:04 +03:00
committed by GitHub
parent 8f9a8ddce0
commit bc072570d2
53 changed files with 1810 additions and 288 deletions
@@ -0,0 +1,177 @@
import {useCallback} from 'react'
import {View} from 'react-native'
import {Image} from 'expo-image'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isWeb} from '#/platform/detection'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle'
import {Text} from '#/components/Typography'
export function ActivitySubscriptionsNUX() {
const t = useTheme()
const {_} = useLingui()
const nuxDialogs = useNuxDialogContext()
const control = Dialog.useDialogControl()
Dialog.useAutoOpen(control)
const onClose = useCallback(() => {
nuxDialogs.dismissActiveNux()
}, [nuxDialogs])
return (
<Dialog.Outer control={control} onClose={onClose}>
<Dialog.Handle />
<Dialog.ScrollableInner
label={_(msg`Introducing activity notifications`)}
style={[web({maxWidth: 400})]}
contentContainerStyle={[
{
paddingTop: 0,
paddingLeft: 0,
paddingRight: 0,
},
]}>
<View
style={[
a.align_center,
a.overflow_hidden,
t.atoms.bg_contrast_25,
{
gap: isWeb ? 16 : 24,
paddingTop: isWeb ? 24 : 48,
borderTopLeftRadius: a.rounded_md.borderRadius,
borderTopRightRadius: a.rounded_md.borderRadius,
},
]}>
<View
style={[
a.pl_sm,
a.pr_md,
a.py_sm,
a.rounded_full,
a.flex_row,
a.align_center,
a.gap_xs,
{
backgroundColor: t.palette.primary_100,
},
]}>
<SparkleIcon fill={t.palette.primary_800} size="sm" />
<Text
style={[
a.font_bold,
{
color: t.palette.primary_800,
},
]}>
<Trans>New Feature</Trans>
</Text>
</View>
<View style={[a.relative, a.w_full]}>
<View
style={[
a.absolute,
t.atoms.bg_contrast_25,
t.atoms.shadow_md,
{
shadowOpacity: 0.4,
top: 5,
bottom: 0,
left: '17%',
right: '17%',
width: '66%',
borderTopLeftRadius: 40,
borderTopRightRadius: 40,
},
]}
/>
<View
style={[
a.overflow_hidden,
{
aspectRatio: 398 / 228,
},
]}>
<Image
accessibilityIgnoresInvertColors
source={require('../../../../assets/images/activity_notifications_announcement.webp')}
style={[
a.w_full,
{
aspectRatio: 398 / 268,
},
]}
alt={_(
msg`A screenshot of a profile page with a bell icon next to the follow button, indicating the new activity notifications feature.`,
)}
/>
</View>
</View>
</View>
<View
style={[
a.align_center,
a.px_xl,
isWeb ? [a.pt_xl, a.gap_xl, a.pb_sm] : [a.pt_3xl, a.gap_3xl],
]}>
<View style={[a.gap_md, a.align_center]}>
<Text
style={[
a.text_3xl,
a.leading_tight,
a.font_heavy,
a.text_center,
{
fontSize: isWeb ? 28 : 32,
maxWidth: 300,
},
]}>
<Trans>Get notified when someone posts</Trans>
</Text>
<Text
style={[
a.text_md,
a.leading_snug,
a.text_center,
{
maxWidth: 340,
},
]}>
<Trans>
You can now choose to be notified when specific people post. If
theres someone you want timely updates from, go to their
profile and find the new bell icon near the follow button.
</Trans>
</Text>
</View>
{!isWeb && (
<Button
label={_(msg`Close`)}
size="large"
variant="solid"
color="primary"
onPress={() => {
control.close()
}}
style={[a.w_full, {maxWidth: 280}]}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
+10 -9
View File
@@ -11,12 +11,12 @@ import {
import {useProfileQuery} from '#/state/queries/profile'
import {type SessionAccount, useSession} from '#/state/session'
import {useOnboardingState} from '#/state/shell'
import {InitialVerificationAnnouncement} from '#/components/dialogs/nuxs/InitialVerificationAnnouncement'
import {ActivitySubscriptionsNUX} from '#/components/dialogs/nuxs/ActivitySubscriptions'
/*
* NUXs
*/
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
import {isDaysOld} from '#/components/dialogs/nuxs/utils'
import {isExistingUserAsOf} from '#/components/dialogs/nuxs/utils'
type Context = {
activeNux: Nux | undefined
@@ -33,9 +33,12 @@ const queuedNuxs: {
}) => boolean
}[] = [
{
id: Nux.InitialVerificationAnnouncement,
id: Nux.ActivitySubscriptions,
enabled: ({currentProfile}) => {
return isDaysOld(2, currentProfile.createdAt)
return isExistingUserAsOf(
'2025-07-03T00:00:00.000Z',
currentProfile.createdAt,
)
},
},
]
@@ -111,7 +114,7 @@ function Inner({
}
React.useEffect(() => {
if (snoozed) return
if (snoozed) return // comment this out to test
if (!nuxs) return
for (const {id, enabled} of queuedNuxs) {
@@ -119,7 +122,7 @@ function Inner({
// check if completed first
if (nux && nux.completed) {
continue
continue // comment this out to test
}
// then check gate (track exposure)
@@ -172,9 +175,7 @@ function Inner({
return (
<Context.Provider value={ctx}>
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
{activeNux === Nux.InitialVerificationAnnouncement && (
<InitialVerificationAnnouncement />
)}
{activeNux === Nux.ActivitySubscriptions && <ActivitySubscriptionsNUX />}
</Context.Provider>
)
}
+15
View File
@@ -16,3 +16,18 @@ export function isDaysOld(days: number, createdAt?: string) {
if (isOldEnough) return true
return false
}
export function isExistingUserAsOf(date: string, createdAt?: string) {
/*
* Should never happen because we gate NUXs to only accounts with a valid
* profile and a `createdAt` (see `nuxs/index.tsx`). But if it ever did, the
* account is either old enough to be pre-onboarding, or some failure happened
* during account creation. Fail closed. - esb
*/
if (!createdAt) return false
const threshold = Date.parse(date)
const then = new Date(createdAt).getTime()
return then < threshold
}