about summary refs log tree commit diff
path: root/src/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens')
-rw-r--r--src/screens/Notifications/ActivityList.tsx44
-rw-r--r--src/screens/Profile/Header/ProfileHeaderStandard.tsx27
-rw-r--r--src/screens/Settings/AccessibilitySettings.tsx19
-rw-r--r--src/screens/Settings/ActivityPrivacySettings.tsx140
-rw-r--r--src/screens/Settings/AppPasswords.tsx6
-rw-r--r--src/screens/Settings/AppearanceSettings.tsx9
-rw-r--r--src/screens/Settings/ExternalMediaPreferences.tsx7
-rw-r--r--src/screens/Settings/FollowingFeedPreferences.tsx5
-rw-r--r--src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx263
-rw-r--r--src/screens/Settings/NotificationSettings/components/PreferenceControls.tsx30
-rw-r--r--src/screens/Settings/NotificationSettings/index.tsx9
-rw-r--r--src/screens/Settings/PrivacyAndSecuritySettings.tsx50
-rw-r--r--src/screens/Settings/Settings.tsx39
-rw-r--r--src/screens/Settings/components/SettingsList.tsx23
14 files changed, 596 insertions, 75 deletions
diff --git a/src/screens/Notifications/ActivityList.tsx b/src/screens/Notifications/ActivityList.tsx
new file mode 100644
index 000000000..f87e34008
--- /dev/null
+++ b/src/screens/Notifications/ActivityList.tsx
@@ -0,0 +1,44 @@
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {type AllNavigatorParams} from '#/lib/routes/types'
+import {PostFeed} from '#/view/com/posts/PostFeed'
+import {EmptyState} from '#/view/com/util/EmptyState'
+import * as Layout from '#/components/Layout'
+import {ListFooter} from '#/components/Lists'
+
+type Props = NativeStackScreenProps<
+  AllNavigatorParams,
+  'NotificationsActivityList'
+>
+export function NotificationsActivityListScreen({
+  route: {
+    params: {posts},
+  },
+}: Props) {
+  const uris = decodeURIComponent(posts)
+  const {_} = useLingui()
+
+  return (
+    <Layout.Screen testID="NotificationsActivityListScreen">
+      <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>
+      <PostFeed
+        feed={`posts|${uris}`}
+        disablePoll
+        renderEmptyState={() => (
+          <EmptyState icon="growth" message={_(msg`No posts here`)} />
+        )}
+        renderEndOfFeed={() => <ListFooter />}
+      />
+    </Layout.Screen>
+  )
+}
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index 1639abaf0..5dbf32c57 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -15,7 +15,6 @@ import {sanitizeHandle} from '#/lib/strings/handles'
 import {logger} from '#/logger'
 import {isIOS} from '#/platform/detection'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {type Shadow} from '#/state/cache/types'
 import {
   useProfileBlockMutationQueue,
   useProfileFollowMutationQueue,
@@ -24,6 +23,7 @@ import {useRequireAuth, useSession} from '#/state/session'
 import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
+import {SubscribeProfileButton} from '#/components/activity-notifications/SubscribeProfileButton'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {useDialogControl} from '#/components/Dialog'
 import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
@@ -58,8 +58,8 @@ let ProfileHeaderStandard = ({
 }: Props): React.ReactNode => {
   const t = useTheme()
   const {gtMobile} = useBreakpoints()
-  const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
-    useProfileShadow(profileUnshadowed)
+  const profile =
+    useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed)
   const {currentAccount, hasSession} = useSession()
   const {_} = useLingui()
   const moderation = useMemo(
@@ -134,13 +134,26 @@ let ProfileHeaderStandard = ({
     }
   }, [_, queueUnblock])
 
-  const isMe = React.useMemo(
+  const isMe = useMemo(
     () => currentAccount?.did === profile.did,
     [currentAccount, profile],
   )
 
   const {isActive: live} = useActorStatus(profile)
 
+  const subscriptionsAllowed = useMemo(() => {
+    switch (profile.associated?.activitySubscription?.allowSubscriptions) {
+      case 'followers':
+      case undefined:
+        return !!profile.viewer?.following
+      case 'mutuals':
+        return !!profile.viewer?.following && !!profile.viewer.followedBy
+      case 'none':
+      default:
+        return false
+    }
+  }, [profile])
+
   return (
     <ProfileHeaderShell
       profile={profile}
@@ -198,6 +211,12 @@ let ProfileHeaderStandard = ({
             )
           ) : !profile.viewer?.blockedBy ? (
             <>
+              {hasSession && subscriptionsAllowed && (
+                <SubscribeProfileButton
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                />
+              )}
               {hasSession && <MessageProfileButton profile={profile} />}
 
               <Button
diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx
index ee26697d2..dbabd2f6f 100644
--- a/src/screens/Settings/AccessibilitySettings.tsx
+++ b/src/screens/Settings/AccessibilitySettings.tsx
@@ -1,8 +1,8 @@
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {isNative} from '#/platform/detection'
 import {
   useHapticsDisabled,
@@ -16,12 +16,10 @@ import {
 } from '#/state/preferences/large-alt-badge'
 import * as SettingsList from '#/screens/Settings/components/SettingsList'
 import {atoms as a} from '#/alf'
-import {Admonition} from '#/components/Admonition'
 import * as Toggle from '#/components/forms/Toggle'
 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
 import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
 import * as Layout from '#/components/Layout'
-import {InlineLinkText} from '#/components/Link'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -100,19 +98,6 @@ export function AccessibilitySettingsScreen({}: Props) {
               </SettingsList.Group>
             </>
           )}
-          <SettingsList.Item>
-            <Admonition type="info" style={[a.flex_1]}>
-              <Trans>
-                Autoplay options have moved to the{' '}
-                <InlineLinkText
-                  to="/settings/content-and-media"
-                  label={_(msg`Content and media`)}>
-                  Content and Media settings
-                </InlineLinkText>
-                .
-              </Trans>
-            </Admonition>
-          </SettingsList.Item>
         </SettingsList.Container>
       </Layout.Content>
     </Layout.Screen>
diff --git a/src/screens/Settings/ActivityPrivacySettings.tsx b/src/screens/Settings/ActivityPrivacySettings.tsx
new file mode 100644
index 000000000..988195a36
--- /dev/null
+++ b/src/screens/Settings/ActivityPrivacySettings.tsx
@@ -0,0 +1,140 @@
+import {View} from 'react-native'
+import {type AppBskyNotificationDeclaration} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {
+  type AllNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {
+  useNotificationDeclarationMutation,
+  useNotificationDeclarationQuery,
+} from '#/state/queries/activity-subscriptions'
+import {atoms as a, useTheme} from '#/alf'
+import {Admonition} from '#/components/Admonition'
+import * as Toggle from '#/components/forms/Toggle'
+import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
+import * as Layout from '#/components/Layout'
+import {Loader} from '#/components/Loader'
+import * as SettingsList from './components/SettingsList'
+import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
+
+type Props = NativeStackScreenProps<
+  AllNavigatorParams,
+  'ActivityPrivacySettings'
+>
+export function ActivityPrivacySettingsScreen({}: Props) {
+  const {
+    data: notificationDeclaration,
+    isPending,
+    isError,
+  } = useNotificationDeclarationQuery()
+
+  return (
+    <Layout.Screen>
+      <Layout.Header.Outer>
+        <Layout.Header.BackButton />
+        <Layout.Header.Content>
+          <Layout.Header.TitleText>
+            <Trans>Privacy and Security</Trans>
+          </Layout.Header.TitleText>
+        </Layout.Header.Content>
+        <Layout.Header.Slot />
+      </Layout.Header.Outer>
+      <Layout.Content>
+        <SettingsList.Container>
+          <SettingsList.Item style={[a.align_start]}>
+            <SettingsList.ItemIcon icon={BellRingingIcon} />
+            <ItemTextWithSubtitle
+              bold
+              titleText={
+                <Trans>Allow others to be notified of your posts</Trans>
+              }
+              subtitleText={
+                <Trans>
+                  This feature allows users to receive notifications for your
+                  new posts and replies. Who do you want to enable this for?
+                </Trans>
+              }
+            />
+          </SettingsList.Item>
+          <View style={[a.px_xl, a.pt_md]}>
+            {isError ? (
+              <Admonition type="error">
+                <Trans>Failed to load preference.</Trans>
+              </Admonition>
+            ) : isPending ? (
+              <View style={[a.w_full, a.pt_5xl, a.align_center]}>
+                <Loader size="xl" />
+              </View>
+            ) : (
+              <Inner notificationDeclaration={notificationDeclaration} />
+            )}
+          </View>
+        </SettingsList.Container>
+      </Layout.Content>
+    </Layout.Screen>
+  )
+}
+
+export function Inner({
+  notificationDeclaration,
+}: {
+  notificationDeclaration: {
+    uri?: string
+    cid?: string
+    value: AppBskyNotificationDeclaration.Record
+  }
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {mutate} = useNotificationDeclarationMutation()
+
+  const onChangeFilter = ([declaration]: string[]) => {
+    mutate({
+      $type: 'app.bsky.notification.declaration',
+      allowSubscriptions: declaration,
+    })
+  }
+
+  return (
+    <Toggle.Group
+      type="radio"
+      label={_(
+        msg`Filter who can opt to receive notifications for your activity`,
+      )}
+      values={[notificationDeclaration.value.allowSubscriptions]}
+      onChange={onChangeFilter}>
+      <View style={[a.gap_sm]}>
+        <Toggle.Item
+          label={_(msg`Anyone who follows me`)}
+          name="followers"
+          style={[a.flex_row, a.py_xs, a.gap_sm]}>
+          <Toggle.Radio />
+          <Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
+            <Trans>Anyone who follows me</Trans>
+          </Toggle.LabelText>
+        </Toggle.Item>
+        <Toggle.Item
+          label={_(msg`Only followers who I follow`)}
+          name="mutuals"
+          style={[a.flex_row, a.py_xs, a.gap_sm]}>
+          <Toggle.Radio />
+          <Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
+            <Trans>Only followers who I follow</Trans>
+          </Toggle.LabelText>
+        </Toggle.Item>
+        <Toggle.Item
+          label={_(msg`No one`)}
+          name="none"
+          style={[a.flex_row, a.py_xs, a.gap_sm]}>
+          <Toggle.Radio />
+          <Toggle.LabelText style={[t.atoms.text, a.font_normal, a.text_md]}>
+            <Trans>No one</Trans>
+          </Toggle.LabelText>
+        </Toggle.Item>
+      </View>
+    </Toggle.Group>
+  )
+}
diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx
index 9a900a3ee..05ebcd80d 100644
--- a/src/screens/Settings/AppPasswords.tsx
+++ b/src/screens/Settings/AppPasswords.tsx
@@ -7,12 +7,12 @@ import Animated, {
   LinearTransition,
   StretchOutY,
 } from 'react-native-reanimated'
-import {ComAtprotoServerListAppPasswords} from '@atproto/api'
+import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {isWeb} from '#/platform/detection'
 import {
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index 4a8a61cd2..d0158aaa8 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback} from 'react'
+import {useCallback} from 'react'
 import Animated, {
   FadeInUp,
   FadeOutUp,
@@ -9,14 +9,17 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {IS_INTERNAL} from '#/lib/app-info'
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
 import {useGate} from '#/lib/statsig/statsig'
 import {isNative} from '#/platform/detection'
 import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
 import {atoms as a, native, useAlf, useTheme} from '#/alf'
 import * as ToggleButton from '#/components/forms/ToggleButton'
-import {Props as SVGIconProps} from '#/components/icons/common'
+import {type Props as SVGIconProps} from '#/components/icons/common'
 import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon'
 import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone'
 import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/TextSize'
diff --git a/src/screens/Settings/ExternalMediaPreferences.tsx b/src/screens/Settings/ExternalMediaPreferences.tsx
index ae859295f..1f0040fb3 100644
--- a/src/screens/Settings/ExternalMediaPreferences.tsx
+++ b/src/screens/Settings/ExternalMediaPreferences.tsx
@@ -2,9 +2,12 @@ import {Fragment} from 'react'
 import {View} from 'react-native'
 import {Trans} from '@lingui/macro'
 
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
 import {
-  EmbedPlayerSource,
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
+import {
+  type EmbedPlayerSource,
   externalEmbedLabels,
 } from '#/lib/strings/embed-player'
 import {
diff --git a/src/screens/Settings/FollowingFeedPreferences.tsx b/src/screens/Settings/FollowingFeedPreferences.tsx
index ea9455ab1..7f1ae1d32 100644
--- a/src/screens/Settings/FollowingFeedPreferences.tsx
+++ b/src/screens/Settings/FollowingFeedPreferences.tsx
@@ -1,7 +1,10 @@
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
+import {
+  type CommonNavigatorParams,
+  type NativeStackScreenProps,
+} from '#/lib/routes/types'
 import {
   usePreferencesQuery,
   useSetFeedViewPreferencesMutation,
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 &rarr; 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`,
diff --git a/src/screens/Settings/PrivacyAndSecuritySettings.tsx b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
index 61a8f81cc..a85ad8372 100644
--- a/src/screens/Settings/PrivacyAndSecuritySettings.tsx
+++ b/src/screens/Settings/PrivacyAndSecuritySettings.tsx
@@ -1,14 +1,17 @@
 import {View} from 'react-native'
+import {type AppBskyNotificationDeclaration} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
 import {type CommonNavigatorParams} from '#/lib/routes/types'
+import {useNotificationDeclarationQuery} from '#/state/queries/activity-subscriptions'
 import {useAppPasswordsQuery} from '#/state/queries/app-passwords'
 import {useSession} from '#/state/session'
 import * as SettingsList from '#/screens/Settings/components/SettingsList'
 import {atoms as a, useTheme} from '#/alf'
 import * as Admonition from '#/components/Admonition'
+import {BellRinging_Stroke2_Corner0_Rounded as BellRingingIcon} from '#/components/icons/BellRinging'
 import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlashIcon} from '#/components/icons/EyeSlash'
 import {Key_Stroke2_Corner2_Rounded as KeyIcon} from '#/components/icons/Key'
 import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
@@ -16,6 +19,7 @@ import * as Layout from '#/components/Layout'
 import {InlineLinkText} from '#/components/Link'
 import {Email2FAToggle} from './components/Email2FAToggle'
 import {PwiOptOut} from './components/PwiOptOut'
+import {ItemTextWithSubtitle} from './NotificationSettings/components/ItemTextWithSubtitle'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
@@ -26,6 +30,11 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
   const t = useTheme()
   const {data: appPasswords} = useAppPasswordsQuery()
   const {currentAccount} = useSession()
+  const {
+    data: notificationDeclaration,
+    isPending,
+    isError,
+  } = useNotificationDeclarationQuery()
 
   return (
     <Layout.Screen>
@@ -71,6 +80,24 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
               </SettingsList.BadgeText>
             )}
           </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            label={_(msg`Settings for activity alerts`)}
+            to={{screen: 'ActivityPrivacySettings'}}
+            contentContainerStyle={[a.align_start]}>
+            <SettingsList.ItemIcon icon={BellRingingIcon} />
+            <ItemTextWithSubtitle
+              titleText={
+                <Trans>Allow others to be notified of your posts</Trans>
+              }
+              subtitleText={
+                <NotificationDeclaration
+                  data={notificationDeclaration}
+                  isError={isError}
+                />
+              }
+              showSkeleton={isPending}
+            />
+          </SettingsList.LinkItem>
           <SettingsList.Divider />
           <SettingsList.Group>
             <SettingsList.ItemIcon icon={EyeSlashIcon} />
@@ -111,3 +138,26 @@ export function PrivacyAndSecuritySettingsScreen({}: Props) {
     </Layout.Screen>
   )
 }
+
+function NotificationDeclaration({
+  data,
+  isError,
+}: {
+  data?: {
+    value: AppBskyNotificationDeclaration.Record
+  }
+  isError?: boolean
+}) {
+  if (isError) {
+    return <Trans>Error loading preference</Trans>
+  }
+  switch (data?.value?.allowSubscriptions) {
+    case 'mutuals':
+      return <Trans>Only followers who I follow</Trans>
+    case 'none':
+      return <Trans>No one</Trans>
+    case 'followers':
+    default:
+      return <Trans>Anyone who follows me</Trans>
+  }
+}
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index e1d197070..aaba0b4b5 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -3,7 +3,7 @@ import {LayoutAnimation, Pressable, View} from 'react-native'
 import {Linking} from 'react-native'
 import {useReducedMotion} from 'react-native-reanimated'
 import {type AppBskyActorDefs, moderateProfile} from '@atproto/api'
-import {msg, t, Trans} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -16,7 +16,6 @@ import {
   type CommonNavigatorParams,
   type NavigationProp,
 } from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -64,6 +63,7 @@ import {
   shouldShowVerificationCheckButton,
   VerificationCheckButton,
 } from '#/components/verification/VerificationCheckButton'
+import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
 export function SettingsScreen({}: Props) {
@@ -82,7 +82,6 @@ export function SettingsScreen({}: Props) {
   const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
   const [showAccounts, setShowAccounts] = useState(false)
   const [showDevOptions, setShowDevOptions] = useState(false)
-  const gate = useGate()
 
   return (
     <Layout.Screen>
@@ -183,16 +182,14 @@ export function SettingsScreen({}: Props) {
               <Trans>Moderation</Trans>
             </SettingsList.ItemText>
           </SettingsList.LinkItem>
-          {gate('reengagement_features') && (
-            <SettingsList.LinkItem
-              to="/settings/notifications"
-              label={_(msg`Notifications`)}>
-              <SettingsList.ItemIcon icon={NotificationIcon} />
-              <SettingsList.ItemText>
-                <Trans>Notifications</Trans>
-              </SettingsList.ItemText>
-            </SettingsList.LinkItem>
-          )}
+          <SettingsList.LinkItem
+            to="/settings/notifications"
+            label={_(msg`Notifications`)}>
+            <SettingsList.ItemIcon icon={NotificationIcon} />
+            <SettingsList.ItemText>
+              <Trans>Notifications</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
           <SettingsList.LinkItem
             to="/settings/content-and-media"
             label={_(msg`Content and media`)}>
@@ -364,6 +361,7 @@ function DevOptions() {
   const onboardingDispatch = useOnboardingDispatch()
   const navigation = useNavigation<NavigationProp>()
   const {mutate: deleteChatDeclarationRecord} = useDeleteActorDeclaration()
+  const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
 
   const resetOnboarding = async () => {
     navigation.navigate('Home')
@@ -384,7 +382,11 @@ function DevOptions() {
       ...persisted.get('reminders'),
       lastEmailConfirm: lastEmailConfirm.toISOString(),
     })
-    Toast.show(t`You probably want to restart the app now.`)
+    Toast.show(_(msg`You probably want to restart the app now.`))
+  }
+
+  const onPressActySubsUnNudge = () => {
+    setActyNotifNudged(false)
   }
 
   return (
@@ -431,6 +433,15 @@ function DevOptions() {
           <Trans>Unsnooze email reminder</Trans>
         </SettingsList.ItemText>
       </SettingsList.PressableItem>
+      {actyNotifNudged && (
+        <SettingsList.PressableItem
+          onPress={onPressActySubsUnNudge}
+          label={_(msg`Reset activity subscription nudge`)}>
+          <SettingsList.ItemText>
+            <Trans>Reset activity subscription nudge</Trans>
+          </SettingsList.ItemText>
+        </SettingsList.PressableItem>
+      )}
       <SettingsList.PressableItem
         onPress={() => clearAllStorage()}
         label={_(msg`Clear all storage data`)}>
diff --git a/src/screens/Settings/components/SettingsList.tsx b/src/screens/Settings/components/SettingsList.tsx
index 520df4118..6d1799047 100644
--- a/src/screens/Settings/components/SettingsList.tsx
+++ b/src/screens/Settings/components/SettingsList.tsx
@@ -1,15 +1,20 @@
-import React, {useContext, useMemo} from 'react'
-import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
+import {createContext, useContext, useMemo} from 'react'
+import {
+  type GestureResponderEvent,
+  type StyleProp,
+  View,
+  type ViewStyle,
+} from 'react-native'
 
 import {HITSLOP_10} from '#/lib/constants'
-import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
+import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
 import * as Button from '#/components/Button'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
-import {Link, LinkProps} from '#/components/Link'
+import {Link, type LinkProps} from '#/components/Link'
 import {createPortalGroup} from '#/components/Portal'
 import {Text} from '#/components/Typography'
 
-const ItemContext = React.createContext({
+const ItemContext = createContext({
   destructive: false,
   withinGroup: false,
 })
@@ -91,7 +96,7 @@ export function Item({
         a.px_xl,
         a.py_sm,
         a.align_center,
-        a.gap_md,
+        a.gap_sm,
         a.w_full,
         a.flex_row,
         {minHeight: 48},
@@ -100,9 +105,9 @@ export function Item({
             // existing padding
             a.pl_xl.paddingLeft +
             // icon
-            28 +
+            24 +
             // gap
-            a.gap_md.gap,
+            a.gap_sm.gap,
         },
         style,
       ]}>
@@ -175,7 +180,7 @@ export function PressableItem({
 
 export function ItemIcon({
   icon: Comp,
-  size = 'xl',
+  size = 'lg',
   color: colorProp,
 }: Omit<React.ComponentProps<typeof Button.ButtonIcon>, 'position'> & {
   color?: string