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>
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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={[
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user