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/ActivityNotificationSettings.tsx | |
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/ActivityNotificationSettings.tsx')
-rw-r--r-- | src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx | 263 |
1 files changed, 263 insertions, 0 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> + ) +} |