Restore activity notification subscriptions screen (#11201)
This commit is contained in:
@@ -328,6 +328,7 @@ func serve(cctx *cli.Context) error {
|
|||||||
e.GET("/settings/interests", server.WebGenericNoindex)
|
e.GET("/settings/interests", server.WebGenericNoindex)
|
||||||
e.GET("/settings/about", server.WebGenericNoindex)
|
e.GET("/settings/about", server.WebGenericNoindex)
|
||||||
e.GET("/settings/notifications", 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", server.WebGenericNoindex)
|
||||||
e.GET("/sys/debug-mod", server.WebGenericNoindex)
|
e.GET("/sys/debug-mod", server.WebGenericNoindex)
|
||||||
e.GET("/sys/log", server.WebGenericNoindex)
|
e.GET("/sys/log", server.WebGenericNoindex)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings'
|
|||||||
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
|
import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
|
||||||
import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
|
import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings'
|
||||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||||
|
import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings'
|
||||||
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
|
import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings'
|
||||||
import {SettingsScreen} from '#/screens/Settings/Settings'
|
import {SettingsScreen} from '#/screens/Settings/Settings'
|
||||||
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
|
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
|
||||||
@@ -451,6 +452,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
|
|||||||
getComponent={() => NotificationSettingsScreen}
|
getComponent={() => NotificationSettingsScreen}
|
||||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ActivityNotificationSettings"
|
||||||
|
getComponent={() => ActivityNotificationSettingsScreen}
|
||||||
|
options={{
|
||||||
|
title: title(msg`Activity notifications`),
|
||||||
|
requireAuth: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ContentAndMediaSettings"
|
name="ContentAndMediaSettings"
|
||||||
getComponent={() => ContentAndMediaSettingsScreen}
|
getComponent={() => ContentAndMediaSettingsScreen}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export type CommonNavigatorParams = {
|
|||||||
ActivityPrivacySettings: undefined
|
ActivityPrivacySettings: undefined
|
||||||
ContentAndMediaSettings: undefined
|
ContentAndMediaSettings: undefined
|
||||||
NotificationSettings: undefined
|
NotificationSettings: undefined
|
||||||
|
ActivityNotificationSettings: undefined
|
||||||
InterestsSettings: undefined
|
InterestsSettings: undefined
|
||||||
AboutSettings: undefined
|
AboutSettings: undefined
|
||||||
AppIconSettings: undefined
|
AppIconSettings: undefined
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export const router = new Router<AllNavigatableRoutes>({
|
|||||||
AboutSettings: '/settings/about',
|
AboutSettings: '/settings/about',
|
||||||
AppIconSettings: '/settings/app-icon',
|
AppIconSettings: '/settings/app-icon',
|
||||||
NotificationSettings: '/settings/notifications',
|
NotificationSettings: '/settings/notifications',
|
||||||
|
ActivityNotificationSettings: '/settings/notifications/activity',
|
||||||
FindContactsSettings: '/settings/find-contacts',
|
FindContactsSettings: '/settings/find-contacts',
|
||||||
// support
|
// support
|
||||||
Support: '/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 → 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 mentionDialogControl = Dialog.useDialogControl()
|
||||||
const quoteDialogControl = Dialog.useDialogControl()
|
const quoteDialogControl = Dialog.useDialogControl()
|
||||||
const repostDialogControl = Dialog.useDialogControl()
|
const repostDialogControl = Dialog.useDialogControl()
|
||||||
const activityDialogControl = Dialog.useDialogControl()
|
|
||||||
const likeRepostDialogControl = Dialog.useDialogControl()
|
const likeRepostDialogControl = Dialog.useDialogControl()
|
||||||
const repostRepostDialogControl = Dialog.useDialogControl()
|
const repostRepostDialogControl = Dialog.useDialogControl()
|
||||||
const chatDialogControl = Dialog.useDialogControl()
|
const chatDialogControl = Dialog.useDialogControl()
|
||||||
@@ -217,9 +216,9 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
showSkeleton={!settings}
|
showSkeleton={!settings}
|
||||||
/>
|
/>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.LinkItem
|
||||||
label={l`Settings for activity from others`}
|
label={l`Settings for activity from others`}
|
||||||
onPress={activityDialogControl.open}
|
to={{screen: 'ActivityNotificationSettings'}}
|
||||||
contentContainerStyle={[a.align_start]}>
|
contentContainerStyle={[a.align_start]}>
|
||||||
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
<SettingsList.ItemIcon icon={BellRingingIcon} />
|
||||||
<ItemTextWithSubtitle
|
<ItemTextWithSubtitle
|
||||||
@@ -229,7 +228,7 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
}
|
}
|
||||||
showSkeleton={!settings}
|
showSkeleton={!settings}
|
||||||
/>
|
/>
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.LinkItem>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
label={l`Settings for notifications for likes of your reposts`}
|
label={l`Settings for notifications for likes of your reposts`}
|
||||||
onPress={likeRepostDialogControl.open}
|
onPress={likeRepostDialogControl.open}
|
||||||
@@ -358,19 +357,6 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
<Trans>Get notifications when people repost your posts.</Trans>
|
<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
|
<NotificationSettingsDialog
|
||||||
control={likeRepostDialogControl}
|
control={likeRepostDialogControl}
|
||||||
name="likeViaRepost"
|
name="likeViaRepost"
|
||||||
|
|||||||
Reference in New Issue
Block a user