about summary refs log tree commit diff
path: root/src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx')
-rw-r--r--src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx263
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 &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>
+  )
+}