diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-07-02 00:36:04 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-07-01 14:36:04 -0700 |
commit | bc072570d27e1f397406daea355570f5aec95647 (patch) | |
tree | 0d698c0bababd9b5e221df763a1ab15744ebdb71 /src/screens/Settings/NotificationSettings | |
parent | 8f9a8ddce022e328b07b793c3f1500e1c423ef73 (diff) | |
download | voidsky-bc072570d27e1f397406daea355570f5aec95647.tar.zst |
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>
Diffstat (limited to 'src/screens/Settings/NotificationSettings')
3 files changed, 280 insertions, 22 deletions
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`, |