Restore activity notification subscriptions screen (#11201)

This commit is contained in:
DS Boyce
2026-07-21 05:16:22 -07:00
committed by GitHub
parent 7d4b91dd38
commit 92ec563f9d
6 changed files with 279 additions and 17 deletions
+1
View File
@@ -328,6 +328,7 @@ func serve(cctx *cli.Context) error {
e.GET("/settings/interests", server.WebGenericNoindex)
e.GET("/settings/about", server.WebGenericNoindex)
e.GET("/settings/notifications", server.WebGenericNoindex)
e.GET("/settings/notifications/activity", server.WebGenericNoindex)
e.GET("/sys/debug", server.WebGenericNoindex)
e.GET("/sys/debug-mod", server.WebGenericNoindex)
e.GET("/sys/log", server.WebGenericNoindex)
+9
View File
@@ -117,6 +117,7 @@ import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
import {SettingsScreen} from '#/screens/Settings/Settings'
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
@@ -451,6 +452,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
getComponent={() => NotificationSettingsScreen}
options={{title: title(msg`Notification settings`), requireAuth: true}}
/>
<Stack.Screen
name="ActivityNotificationSettings"
getComponent={() => ActivityNotificationSettingsScreen}
options={{
title: title(msg`Activity notifications`),
requireAuth: true,
}}
/>
<Stack.Screen
name="ContentAndMediaSettings"
getComponent={() => ContentAndMediaSettingsScreen}
+1
View File
@@ -70,6 +70,7 @@ export type CommonNavigatorParams = {
ActivityPrivacySettings: undefined
ContentAndMediaSettings: undefined
NotificationSettings: undefined
ActivityNotificationSettings: undefined
InterestsSettings: undefined
AboutSettings: undefined
AppIconSettings: undefined
+1
View File
@@ -61,6 +61,7 @@ export const router = new Router<AllNavigatableRoutes>({
AboutSettings: '/settings/about',
AppIconSettings: '/settings/app-icon',
NotificationSettings: '/settings/notifications',
ActivityNotificationSettings: '/settings/notifications/activity',
FindContactsSettings: '/settings/find-contacts',
// support
Support: '/support',
@@ -0,0 +1,264 @@
import {useCallback, useMemo} from 'react'
import {type ListRenderItemInfo, Text as RNText, View} from 'react-native'
import {type ModerationOpts} from '@atproto/api'
import {useLingui} from '@lingui/react/macro'
import {Trans} from '@lingui/react/macro'
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,
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 {t: l} = useLingui()
const {data: preferences, isError: isPreferencesError} =
useNotificationSettingsQuery()
const moderationOpts = useModerationOpts()
const {
data: subscriptions,
isPending,
isError: isSubscriptionsError,
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(() => {
if (isFetchingNextPage || !hasNextPage || isSubscriptionsError) return
void fetchNextPage().catch(err => {
logger.error('Failed to load more activity subscriptions', {
message: err,
})
})
}, [isFetchingNextPage, hasNextPage, isSubscriptionsError, 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={l`Activity from others`}
subtitleText={l`Get notified about posts and replies from accounts you choose.`}
/>
</SettingsList.Item>
{isPreferencesError ? (
<View style={[a.px_xl, a.pt_md]}>
<Admonition.Admonition type="error">
<Trans>Failed to load notification settings.</Trans>
</Admonition.Admonition>
</View>
) : (
<View style={[a.px_xl]}>
<PreferenceControls
name="subscribedPost"
preference={preferences?.subscribedPost}
/>
</View>
)}
</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 />
<Admonition.Content>
<Admonition.Text>
<Trans>
Enable notifications for an account by visiting their
profile and pressing the{' '}
<RNText
style={[
a.font_semi_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={l`Privacy and Security settings`}
to={{screen: 'ActivityPrivacySettings'}}
style={[a.font_semi_bold]}>
Settings &rarr; Privacy and Security
</InlineLinkText>
.
</Trans>
</Admonition.Text>
</Admonition.Content>
</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 {t: l} = useLingui()
const t = useTheme()
const preview = useMemo(() => {
const actSub = profile.viewer?.activitySubscription
if (actSub?.post && actSub?.reply) {
return l`Posts, Replies`
} else if (actSub?.post) {
return l`Posts`
} else if (actSub?.reply) {
return l`Replies`
}
return l`None`
}, [l, 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={l`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>
)
}
@@ -57,7 +57,6 @@ export function NotificationSettingsScreen({}: Props) {
const mentionDialogControl = Dialog.useDialogControl()
const quoteDialogControl = Dialog.useDialogControl()
const repostDialogControl = Dialog.useDialogControl()
const activityDialogControl = Dialog.useDialogControl()
const likeRepostDialogControl = Dialog.useDialogControl()
const repostRepostDialogControl = Dialog.useDialogControl()
const chatDialogControl = Dialog.useDialogControl()
@@ -217,9 +216,9 @@ export function NotificationSettingsScreen({}: Props) {
showSkeleton={!settings}
/>
</SettingsList.PressableItem>
<SettingsList.PressableItem
<SettingsList.LinkItem
label={l`Settings for activity from others`}
onPress={activityDialogControl.open}
to={{screen: 'ActivityNotificationSettings'}}
contentContainerStyle={[a.align_start]}>
<SettingsList.ItemIcon icon={BellRingingIcon} />
<ItemTextWithSubtitle
@@ -229,7 +228,7 @@ export function NotificationSettingsScreen({}: Props) {
}
showSkeleton={!settings}
/>
</SettingsList.PressableItem>
</SettingsList.LinkItem>
<SettingsList.PressableItem
label={l`Settings for notifications for likes of your reposts`}
onPress={likeRepostDialogControl.open}
@@ -358,19 +357,6 @@ export function NotificationSettingsScreen({}: Props) {
<Trans>Get notifications when people repost your posts.</Trans>
}
/>
<NotificationSettingsDialog
control={activityDialogControl}
name="subscribedPost"
icon={BellRingingIcon}
titleText={<Trans>Activity from others</Trans>}
subtitleText={
<Trans>
Get notifications when there's activity on posts you're subscribed
to.
</Trans>
}
allowDisableInApp={false}
/>
<NotificationSettingsDialog
control={likeRepostDialogControl}
name="likeViaRepost"