diff options
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/Notifications/ActivityList.tsx | 44 | ||||
-rw-r--r-- | src/screens/Profile/Header/ProfileHeaderStandard.tsx | 27 | ||||
-rw-r--r-- | src/screens/Settings/AccessibilitySettings.tsx | 19 | ||||
-rw-r--r-- | src/screens/Settings/ActivityPrivacySettings.tsx | 140 | ||||
-rw-r--r-- | src/screens/Settings/AppPasswords.tsx | 6 | ||||
-rw-r--r-- | src/screens/Settings/AppearanceSettings.tsx | 9 | ||||
-rw-r--r-- | src/screens/Settings/ExternalMediaPreferences.tsx | 7 | ||||
-rw-r--r-- | src/screens/Settings/FollowingFeedPreferences.tsx | 5 | ||||
-rw-r--r-- | src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx | 263 | ||||
-rw-r--r-- | src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx | 30 | ||||
-rw-r--r-- | src/screens/Settings/NotificationSettings/index.tsx | 9 | ||||
-rw-r--r-- | src/screens/Settings/PrivacyAndSecuritySettings.tsx | 50 | ||||
-rw-r--r-- | src/screens/Settings/Settings.tsx | 39 | ||||
-rw-r--r-- | src/screens/Settings/components/SettingsList.tsx | 23 |
14 files changed, 596 insertions, 75 deletions
diff --git a/src/screens/Notifications/ActivityList.tsx b/src/screens/Notifications/ActivityList.tsx new file mode 100644 index 000000000..f87e34008 --- /dev/null +++ b/src/screens/Notifications/ActivityList.tsx @@ -0,0 +1,44 @@ +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type AllNavigatorParams} from '#/lib/routes/types' +import {PostFeed} from '#/view/com/posts/PostFeed' +import {EmptyState} from '#/view/com/util/EmptyState' +import * as Layout from '#/components/Layout' +import {ListFooter} from '#/components/Lists' + +type Props = NativeStackScreenProps< + AllNavigatorParams, + 'NotificationsActivityList' +> +export function NotificationsActivityListScreen({ + route: { + params: {posts}, + }, +}: Props) { + const uris = decodeURIComponent(posts) + const {_} = useLingui() + + return ( + <Layout.Screen testID="NotificationsActivityListScreen"> + <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> + <PostFeed + feed={`posts|${uris}`} + disablePoll + renderEmptyState={() => ( + <EmptyState icon="growth" message={_(msg`No posts here`)} /> + )} + renderEndOfFeed={() => <ListFooter />} + /> + </Layout.Screen> + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 1639abaf0..5dbf32c57 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {useProfileShadow} from '#/state/cache/profile-shadow' -import {type Shadow} from '#/state/cache/types' import { useProfileBlockMutationQueue, useProfileFollowMutationQueue, @@ -24,6 +23,7 @@ import {useRequireAuth, useSession} from '#/state/session' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' +import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' import {MessageProfileButton} from '#/components/dms/MessageProfileButton' @@ -58,8 +58,8 @@ let ProfileHeaderStandard = ({ }: Props): React.ReactNode => { const t = useTheme() const {gtMobile} = useBreakpoints() - const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> = - useProfileShadow(profileUnshadowed) + const profile = + useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed) const {currentAccount, hasSession} = useSession() const {_} = useLingui() const moderation = useMemo( @@ -134,13 +134,26 @@ let ProfileHeaderStandard = ({ } }, [_, queueUnblock]) - const isMe = React.useMemo( + const isMe = useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], ) const {isActive: live} = useActorStatus(profile) + const subscriptionsAllowed = useMemo(() => { + switch (profile.associated?.activitySubscription?.allowSubscriptions) { + case 'followers': + case undefined: + return !!profile.viewer?.following + case 'mutuals': + return !!profile.viewer?.following && !!profile.viewer.followedBy + case 'none': + default: + return false + } + }, [profile]) + return ( <ProfileHeaderShell profile={profile} @@ -198,6 +211,12 @@ let ProfileHeaderStandard = ({ ) ) : !profile.viewer?.blockedBy ? ( <> + {hasSession && subscriptionsAllowed && ( + <SubscribeProfileButton + profile={profile} + moderationOpts={moderationOpts} + /> + )} {hasSession && <MessageProfileButton profile={profile} />} <Button diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx index ee26697d2..dbabd2f6f 100644 --- a/src/screens/Settings/AccessibilitySettings.tsx +++ b/src/screens/Settings/AccessibilitySettings.tsx @@ -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> diff --git a/src/screens/Settings/ActivityPrivacySettings.tsx b/src/screens/Settings/ActivityPrivacySettings.tsx new file mode 100644 index 000000000..988195a36 --- /dev/null +++ b/src/screens/Settings/ActivityPrivacySettings.tsx @@ -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> + ) +} diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx index 9a900a3ee..05ebcd80d 100644 --- a/src/screens/Settings/AppPasswords.tsx +++ b/src/screens/Settings/AppPasswords.tsx @@ -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 { diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index 4a8a61cd2..d0158aaa8 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -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' diff --git a/src/screens/Settings/ExternalMediaPreferences.tsx b/src/screens/Settings/ExternalMediaPreferences.tsx index ae859295f..1f0040fb3 100644 --- a/src/screens/Settings/ExternalMediaPreferences.tsx +++ b/src/screens/Settings/ExternalMediaPreferences.tsx @@ -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 { diff --git a/src/screens/Settings/FollowingFeedPreferences.tsx b/src/screens/Settings/FollowingFeedPreferences.tsx index ea9455ab1..7f1ae1d32 100644 --- a/src/screens/Settings/FollowingFeedPreferences.tsx +++ b/src/screens/Settings/FollowingFeedPreferences.tsx @@ -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, diff --git a/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx new file mode 100644 index 000000000..b00170f3a --- /dev/null +++ b/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx @@ -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 → 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> + ) +} diff --git a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx index 487827d66..ce46541fd 100644 --- a/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx +++ b/src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx @@ -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={[ diff --git a/src/screens/Settings/NotificationSettings/index.tsx b/src/screens/Settings/NotificationSettings/index.tsx index 800493575..df7c9a35b 100644 --- a/src/screens/Settings/NotificationSettings/index.tsx +++ b/src/screens/Settings/NotificationSettings/index.tsx @@ -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`, diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx index 61a8f81cc..a85ad8372 100644 --- a/src/screens/Settings/PrivacyAndSecuritySettings.tsx +++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx @@ -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> + } +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index e1d197070..aaba0b4b5 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -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`)}> diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx index 520df4118..6d1799047 100644 --- a/src/screens/Settings/components/SettingsList.tsx +++ b/src/screens/Settings/components/SettingsList.tsx @@ -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 |