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
+2 -17
View File
@@ -1,8 +1,8 @@
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {isNative} from '#/platform/detection'
import {
useHapticsDisabled,
@@ -16,12 +16,10 @@ import {
} from '#/state/preferences/large-alt-badge'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import * as Toggle from '#/components/forms/Toggle'
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -100,19 +98,6 @@ export function AccessibilitySettingsScreen({}: Props) {
</SettingsList.Group>
</>
)}
<SettingsList.Item>
<Admonition type="info" style={[a.flex_1]}>
<Trans>
Autoplay options have moved to the{' '}
<InlineLinkText
to="/settings/content-and-media"
label={_(msg`Content and media`)}>
Content and Media settings
</InlineLinkText>
.
</Trans>
</Admonition>
</SettingsList.Item>
</SettingsList.Container>
</Layout.Content>
</Layout.Screen>
@@ -0,0 +1,140 @@
import {View} from 'react-native'
import {type AppBskyNotificationDeclaration} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
type AllNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {
useNotificationDeclarationMutation,
useNotificationDeclarationQuery,
} from '#/state/queries/activity-subscriptions'
import {atoms as a, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import * as Toggle from '#/components/forms/Toggle'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
import * as SettingsList from './components/SettingsList'
import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
type Props = NativeStackScreenProps<
AllNavigatorParams,
'ActivityPrivacySettings'
>
export function ActivityPrivacySettingsScreen({}: Props) {
const {
data: notificationDeclaration,
isPending,
isError,
} = useNotificationDeclarationQuery()
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Privacy and Security</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<SettingsList.Item style={[a.align_start]}>
<SettingsList.ItemIcon icon={BellRingingIcon} />
<ItemTextWithSubtitle
bold
titleText={
<Trans>Allow others to be notified of your posts</Trans>
}
subtitleText={
<Trans>
This feature allows users to receive notifications for your
new posts and replies. Who do you want to enable this for?
</Trans>
}
/>
</SettingsList.Item>
<View style={[a.px_xl, a.pt_md]}>
{isError ? (
<Admonition type="error">
<Trans>Failed to load preference.</Trans>
</Admonition>
) : isPending ? (
<View style={[a.w_full, a.pt_5xl, a.align_center]}>
<Loader size="xl" />
</View>
) : (
<Inner notificationDeclaration={notificationDeclaration} />
)}
</View>
</SettingsList.Container>
</Layout.Content>
</Layout.Screen>
)
}
export function Inner({
notificationDeclaration,
}: {
notificationDeclaration: {
uri?: string
cid?: string
value: AppBskyNotificationDeclaration.Record
}
}) {
const t = useTheme()
const {_} = useLingui()
const {mutate} = useNotificationDeclarationMutation()
const onChangeFilter = ([declaration]: string[]) => {
mutate({
$type: 'app.bsky.notification.declaration',
allowSubscriptions: declaration,
})
}
return (
<Toggle.Group
type="radio"
label={_(
msg`Filter who can opt to receive notifications for your activity`,
)}
values={[notificationDeclaration.value.allowSubscriptions]}
onChange={onChangeFilter}>
<View style={[a.gap_sm]}>
<Toggle.Item
label={_(msg`Anyone who follows me`)}
name="followers"
style={[a.flex_row, a.py_xs, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
<Trans>Anyone who follows me</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item
label={_(msg`Only followers who I follow`)}
name="mutuals"
style={[a.flex_row, a.py_xs, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
<Trans>Only followers who I follow</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Toggle.Item
label={_(msg`No one`)}
name="none"
style={[a.flex_row, a.py_xs, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
<Trans>No one</Trans>
</Toggle.LabelText>
</Toggle.Item>
</View>
</Toggle.Group>
)
}
+3 -3
View File
@@ -7,12 +7,12 @@ import Animated, {
LinearTransition,
StretchOutY,
} from 'react-native-reanimated'
import {ComAtprotoServerListAppPasswords} from '@atproto/api'
import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {isWeb} from '#/platform/detection'
import {
+6 -3
View File
@@ -1,4 +1,4 @@
import React, {useCallback} from 'react'
import {useCallback} from 'react'
import Animated, {
FadeInUp,
FadeOutUp,
@@ -9,14 +9,17 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_INTERNAL} from '#/lib/app-info'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {isNative} from '#/platform/detection'
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
import {atoms as a, native, useAlf, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Props as SVGIconProps} from '#/components/icons/common'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
@@ -2,9 +2,12 @@ import {Fragment} from 'react'
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
EmbedPlayerSource,
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {
type EmbedPlayerSource,
externalEmbedLabels,
} from '#/lib/strings/embed-player'
import {
@@ -1,7 +1,10 @@
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {
usePreferencesQuery,
useSetFeedViewPreferencesMutation,
@@ -0,0 +1,263 @@
import {useCallback, useMemo} from 'react'
import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
import {type ModerationOpts} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import {
type AllNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActivitySubscriptionsQuery} from '#/state/queries/activity-subscriptions'
import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
import {List} from '#/view/com/util/List'
import {atoms as a, useTheme} from '#/alf'
import {SubscribeProfileDialog} from '#/components/activity-notifications/SubscribeProfileDialog'
import * as Admonition from '#/components/Admonition'
import {Button, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {BellRinging_Filled_Corner0_Rounded as BellRingingFilledIcon} from '#/components/icons/BellRinging'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {ListFooter} from '#/components/Lists'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
import * as SettingsList from '../components/SettingsList'
import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle'
import {PreferenceControls} from './components/PreferenceControls'
type Props = NativeStackScreenProps<
AllNavigatorParams,
'ActivityNotificationSettings'
>
export function ActivityNotificationSettingsScreen({}: Props) {
const t = useTheme()
const {_} = useLingui()
const {data: preferences, isError} = useNotificationSettingsQuery()
const moderationOpts = useModerationOpts()
const {
data: subscriptions,
isPending,
error,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useActivitySubscriptionsQuery()
const items = useMemo(() => {
if (!subscriptions) return []
return subscriptions?.pages.flatMap(page => page.subscriptions)
}, [subscriptions])
const renderItem = useCallback(
({item}: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
if (!moderationOpts) return null
return (
<ActivitySubscriptionCard
profile={item}
moderationOpts={moderationOpts}
/>
)
},
[moderationOpts],
)
const onEndReached = useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Notifications</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<List
ListHeaderComponent={
<SettingsList.Container>
<SettingsList.Item style={[a.align_start]}>
<SettingsList.ItemIcon icon={BellRingingIcon} />
<ItemTextWithSubtitle
bold
titleText={<Trans>Activity from others</Trans>}
subtitleText={
<Trans>
Get notified about posts and replies from accounts you
choose.
</Trans>
}
/>
</SettingsList.Item>
{isError ? (
<View style={[a.px_lg, a.pt_md]}>
<Admonition.Admonition type="error">
<Trans>Failed to load notification settings.</Trans>
</Admonition.Admonition>
</View>
) : (
<PreferenceControls
name="subscribedPost"
preference={preferences?.subscribedPost}
/>
)}
</SettingsList.Container>
}
data={items}
keyExtractor={keyExtractor}
renderItem={renderItem}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListEmptyComponent={
error ? null : (
<View style={[a.px_xl, a.py_md]}>
{!isPending ? (
<Admonition.Outer type="tip">
<Admonition.Row>
<Admonition.Icon />
<View style={[a.flex_1, a.gap_sm]}>
<Admonition.Text>
<Trans>
Enable notifications for an account by visiting their
profile and pressing the{' '}
<RNText
style={[a.font_bold, t.atoms.text_contrast_high]}>
bell icon
</RNText>{' '}
<BellRingingFilledIcon
size="xs"
style={t.atoms.text_contrast_high}
/>
.
</Trans>
</Admonition.Text>
<Admonition.Text>
<Trans>
If you want to restrict who can receive notifications
for your account's activity, you can change this in{' '}
<InlineLinkText
label={_(msg`Privacy and Security settings`)}
to={{screen: 'ActivityPrivacySettings'}}
style={[a.font_bold]}>
Settings &rarr; Privacy and Security
</InlineLinkText>
.
</Trans>
</Admonition.Text>
</View>
</Admonition.Row>
</Admonition.Outer>
) : (
<View style={[a.flex_1, a.align_center, a.pt_xl]}>
<Loader size="lg" />
</View>
)}
</View>
)
}
ListFooterComponent={
<ListFooter
style={[items.length === 0 && a.border_transparent]}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
hasNextPage={hasNextPage}
/>
}
windowSize={11}
/>
</Layout.Screen>
)
}
function keyExtractor(item: bsky.profile.AnyProfileView) {
return item.did
}
function ActivitySubscriptionCard({
profile: profileUnshadowed,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const profile = useProfileShadow(profileUnshadowed)
const control = useDialogControl()
const {_} = useLingui()
const t = useTheme()
const preview = useMemo(() => {
const actSub = profile.viewer?.activitySubscription
if (actSub?.post && actSub?.reply) {
return _(msg`Posts, Replies`)
} else if (actSub?.post) {
return _(msg`Posts`)
} else if (actSub?.reply) {
return _(msg`Replies`)
}
return _(msg`None`)
}, [_, profile.viewer?.activitySubscription])
return (
<View style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
inline
/>
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
{preview}
</Text>
</View>
<Button
label={_(
msg`Edit notifications from ${createSanitizedDisplayName(
profile,
)}`,
)}
size="small"
color="primary"
variant="solid"
onPress={control.open}>
<ButtonText>
<Trans>Edit</Trans>
</ButtonText>
</Button>
</ProfileCard.Header>
</ProfileCard.Outer>
<SubscribeProfileDialog
control={control}
profile={profile}
moderationOpts={moderationOpts}
includeProfile
/>
</View>
)
}
@@ -5,7 +5,7 @@ import {type FilterablePreference} from '@atproto/api/dist/client/types/app/bsky
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {useNotificationSettingsUpdateMutation} from '#/state/queries/notifications/settings'
import {atoms as a, platform, useTheme} from '#/alf'
import * as Toggle from '#/components/forms/Toggle'
@@ -28,10 +28,6 @@ export function PreferenceControls({
preference?: AppBskyNotificationDefs.Preference | FilterablePreference
allowDisableInApp?: boolean
}) {
const gate = useGate()
if (!gate('reengagement_features')) return null
if (!preference)
return (
<View style={[a.w_full, a.pt_5xl, a.align_center]}>
@@ -78,6 +74,12 @@ export function Inner({
push: change.includes('push'),
} satisfies typeof preference
logger.metric('activityPreference:changeChannels', {
name,
push: newPreference.push,
list: newPreference.list,
})
mutate({
[name]: newPreference,
...Object.fromEntries(syncOthers.map(key => [key, newPreference])),
@@ -93,6 +95,8 @@ export function Inner({
include: change,
} satisfies typeof preference
logger.metric('activityPreference:changeFilter', {name, value: change})
mutate({
[name]: newPreference,
...Object.fromEntries(syncOthers.map(key => [key, newPreference])),
@@ -114,7 +118,7 @@ export function Inner({
a.py_xs,
platform({
native: [a.justify_between],
web: [a.flex_row_reverse, a.gap_md],
web: [a.flex_row_reverse, a.gap_sm],
}),
]}>
<Toggle.LabelText
@@ -131,7 +135,7 @@ export function Inner({
a.py_xs,
platform({
native: [a.justify_between],
web: [a.flex_row_reverse, a.gap_md],
web: [a.flex_row_reverse, a.gap_sm],
}),
]}>
<Toggle.LabelText
@@ -159,11 +163,7 @@ export function Inner({
<Toggle.Item
label={_(msg`Everyone`)}
name="all"
style={[
a.flex_row,
a.py_xs,
platform({native: [a.gap_sm], web: [a.gap_md]}),
]}>
style={[a.flex_row, a.py_xs, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText
style={[
@@ -177,11 +177,7 @@ export function Inner({
<Toggle.Item
label={_(msg`People I follow`)}
name="follows"
style={[
a.flex_row,
a.py_xs,
platform({native: [a.gap_sm], web: [a.gap_md]}),
]}>
style={[a.flex_row, a.py_xs, a.gap_sm]}>
<Toggle.Radio />
<Toggle.LabelText
style={[
@@ -16,7 +16,7 @@ import {useNotificationSettingsQuery} from '#/state/queries/notifications/settin
import {atoms as a} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At'
// import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble'
import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
import {
@@ -183,20 +183,19 @@ export function NotificationSettingsScreen({}: Props) {
showSkeleton={!settings}
/>
</SettingsList.LinkItem>
{/* <SettingsList.LinkItem
<SettingsList.LinkItem
label={_(msg`Settings for activity alerts`)}
to={{screen: 'ActivityNotificationSettings'}}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={BellRingingIcon} />
<ItemTextWithSubtitle
titleText={<Trans>Activity alerts</Trans>}
titleText={<Trans>Activity from others</Trans>}
subtitleText={
<SettingPreview preference={settings?.subscribedPost} />
}
showSkeleton={!settings}
/>
</SettingsList.LinkItem> */}
</SettingsList.LinkItem>
<SettingsList.LinkItem
label={_(
msg`Settings for notifications for likes of your reposts`,
@@ -1,14 +1,17 @@
import {View} from 'react-native'
import {type AppBskyNotificationDeclaration} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {useNotificationDeclarationQuery} from '#/state/queries/activity-subscriptions'
import {useAppPasswordsQuery} from '#/state/queries/app-passwords'
import {useSession} from '#/state/session'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, useTheme} from '#/alf'
import * as Admonition from '#/components/Admonition'
import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
@@ -16,6 +19,7 @@ import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link'
import {Email2FAToggle} from './components/Email2FAToggle'
import {PwiOptOut} from './components/PwiOptOut'
import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
@@ -26,6 +30,11 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
const t = useTheme()
const {data: appPasswords} = useAppPasswordsQuery()
const {currentAccount} = useSession()
const {
data: notificationDeclaration,
isPending,
isError,
} = useNotificationDeclarationQuery()
return (
<Layout.Screen>
@@ -71,6 +80,24 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
</SettingsList.BadgeText>
)}
</SettingsList.LinkItem>
<SettingsList.LinkItem
label={_(msg`Settings for activity alerts`)}
to={{screen: 'ActivityPrivacySettings'}}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={BellRingingIcon} />
<ItemTextWithSubtitle
titleText={
<Trans>Allow others to be notified of your posts</Trans>
}
subtitleText={
<NotificationDeclaration
data={notificationDeclaration}
isError={isError}
/>
}
showSkeleton={isPending}
/>
</SettingsList.LinkItem>
<SettingsList.Divider />
<SettingsList.Group>
<SettingsList.ItemIcon icon={EyeSlashIcon} />
@@ -111,3 +138,26 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
</Layout.Screen>
)
}
function NotificationDeclaration({
data,
isError,
}: {
data?: {
value: AppBskyNotificationDeclaration.Record
}
isError?: boolean
}) {
if (isError) {
return <Trans>Error loading preference</Trans>
}
switch (data?.value?.allowSubscriptions) {
case 'mutuals':
return <Trans>Only followers who I follow</Trans>
case 'none':
return <Trans>No one</Trans>
case 'followers':
default:
return <Trans>Anyone who follows me</Trans>
}
}
+25 -14
View File
@@ -3,7 +3,7 @@ import {LayoutAnimation, Pressable, View} from 'react-native'
import {Linking} from 'react-native'
import {useReducedMotion} from 'react-native-reanimated'
import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
import {msg, t, Trans} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -16,7 +16,6 @@ import {
type CommonNavigatorParams,
type NavigationProp,
} from '#/lib/routes/types'
import {useGate} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -64,6 +63,7 @@ import {
shouldShowVerificationCheckButton,
VerificationCheckButton,
} from '#/components/verification/VerificationCheckButton'
import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export function SettingsScreen({}: Props) {
@@ -82,7 +82,6 @@ export function SettingsScreen({}: Props) {
const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
const [showAccounts, setShowAccounts] = useState(false)
const [showDevOptions, setShowDevOptions] = useState(false)
const gate = useGate()
return (
<Layout.Screen>
@@ -183,16 +182,14 @@ export function SettingsScreen({}: Props) {
<Trans>Moderation</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
{gate('reengagement_features') && (
<SettingsList.LinkItem
to="/settings/notifications"
label={_(msg`Notifications`)}>
<SettingsList.ItemIcon icon={NotificationIcon} />
<SettingsList.ItemText>
<Trans>Notifications</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
)}
<SettingsList.LinkItem
to="/settings/notifications"
label={_(msg`Notifications`)}>
<SettingsList.ItemIcon icon={NotificationIcon} />
<SettingsList.ItemText>
<Trans>Notifications</Trans>
</SettingsList.ItemText>
</SettingsList.LinkItem>
<SettingsList.LinkItem
to="/settings/content-and-media"
label={_(msg`Content and media`)}>
@@ -364,6 +361,7 @@ function DevOptions() {
const onboardingDispatch = useOnboardingDispatch()
const navigation = useNavigation<NavigationProp>()
const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
const resetOnboarding = async () => {
navigation.navigate('Home')
@@ -384,7 +382,11 @@ function DevOptions() {
...persisted.get('reminders'),
lastEmailConfirm: lastEmailConfirm.toISOString(),
})
Toast.show(t`You probably want to restart the app now.`)
Toast.show(_(msg`You probably want to restart the app now.`))
}
const onPressActySubsUnNudge = () => {
setActyNotifNudged(false)
}
return (
@@ -431,6 +433,15 @@ function DevOptions() {
<Trans>Unsnooze email reminder</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
{actyNotifNudged && (
<SettingsList.PressableItem
onPress={onPressActySubsUnNudge}
label={_(msg`Reset activity subscription nudge`)}>
<SettingsList.ItemText>
<Trans>Reset activity subscription nudge</Trans>
</SettingsList.ItemText>
</SettingsList.PressableItem>
)}
<SettingsList.PressableItem
onPress={() => clearAllStorage()}
label={_(msg`Clear all storage data`)}>
@@ -1,15 +1,20 @@
import React, {useContext, useMemo} from 'react'
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
import {createContext, useContext, useMemo} from 'react'
import {
type GestureResponderEvent,
type StyleProp,
View,
type ViewStyle,
} from 'react-native'
import {HITSLOP_10} from '#/lib/constants'
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import * as Button from '#/components/Button'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
import {Link, LinkProps} from '#/components/Link'
import {Link, type LinkProps} from '#/components/Link'
import {createPortalGroup} from '#/components/Portal'
import {Text} from '#/components/Typography'
const ItemContext = React.createContext({
const ItemContext = createContext({
destructive: false,
withinGroup: false,
})
@@ -91,7 +96,7 @@ export function Item({
a.px_xl,
a.py_sm,
a.align_center,
a.gap_md,
a.gap_sm,
a.w_full,
a.flex_row,
{minHeight: 48},
@@ -100,9 +105,9 @@ export function Item({
// existing padding
a.pl_xl.paddingLeft +
// icon
28 +
24 +
// gap
a.gap_md.gap,
a.gap_sm.gap,
},
style,
]}>
@@ -175,7 +180,7 @@ export function PressableItem({
export function ItemIcon({
icon: Comp,
size = 'xl',
size = 'lg',
color: colorProp,
}: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & {
color?: string