about summary refs log tree commit diff
path: root/src/components
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-03-19 19:51:35 +0000
committerSamuel Newman <mozzius@protonmail.com>2024-03-19 19:51:35 +0000
commit4794ab6b9a39d06bb0d1b7c64a315e4ac5e3336a (patch)
tree743b94dff5a4a3b4b2304c53b984b8a3dc67aaa2 /src/components
parenta1c4f19731878f7026d398d28e475bbeb7de824a (diff)
parent5621c8042510c86f6c4fa63b5c5ce9fc02b0bf8e (diff)
downloadvoidsky-4794ab6b9a39d06bb0d1b7c64a315e4ac5e3336a.tar.zst
Merge remote-tracking branch 'origin/main' into samuel/alf-login
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Dialog/types.ts10
-rw-r--r--src/components/Error.tsx90
-rw-r--r--src/components/LabelingServiceCard/index.tsx2
-rw-r--r--src/components/Lists.tsx183
-rw-r--r--src/components/Menu/index.tsx6
-rw-r--r--src/components/ReportDialog/SelectLabelerView.tsx75
-rw-r--r--src/components/ReportDialog/SelectReportOptionView.tsx5
-rw-r--r--src/components/TagMenu/index.tsx4
-rw-r--r--src/components/dialogs/BirthDateSettings.tsx74
-rw-r--r--src/components/moderation/ModerationLabelPref.tsx97
10 files changed, 304 insertions, 242 deletions
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 9e7ad3c04..b1a46f853 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -1,5 +1,5 @@
 import React from 'react'
-import type {AccessibilityProps} from 'react-native'
+import type {AccessibilityProps, GestureResponderEvent} from 'react-native'
 import {BottomSheetProps} from '@gorhom/bottom-sheet'
 
 import {ViewStyleProp} from '#/alf'
@@ -10,9 +10,15 @@ type A11yProps = Required<AccessibilityProps>
  * Mutated by useImperativeHandle to provide a public API for controlling the
  * dialog. The methods here will actually become the handlers defined within
  * the `Dialog.Outer` component.
+ *
+ * `Partial<GestureResponderEvent>` here allows us to add this directly to the
+ * `onPress` prop of a button, for example. If this type was not added, we
+ * would need to create a function to wrap `.open()` with.
  */
 export type DialogControlRefProps = {
-  open: (options?: DialogControlOpenOptions) => void
+  open: (
+    options?: DialogControlOpenOptions & Partial<GestureResponderEvent>,
+  ) => void
   close: (callback?: () => void) => void
 }
 
diff --git a/src/components/Error.tsx b/src/components/Error.tsx
new file mode 100644
index 000000000..1dbf68284
--- /dev/null
+++ b/src/components/Error.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+
+import {CenteredView} from 'view/com/util/Views'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {View} from 'react-native'
+import {Button} from '#/components/Button'
+import {useNavigation} from '@react-navigation/core'
+import {NavigationProp} from 'lib/routes/types'
+import {StackActions} from '@react-navigation/native'
+import {router} from '#/routes'
+
+export function Error({
+  title,
+  message,
+  onRetry,
+}: {
+  title?: string
+  message?: string
+  onRetry?: () => unknown
+}) {
+  const navigation = useNavigation<NavigationProp>()
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
+
+  const canGoBack = navigation.canGoBack()
+  const onGoBack = React.useCallback(() => {
+    if (canGoBack) {
+      navigation.goBack()
+    } else {
+      navigation.navigate('HomeTab')
+
+      // Checking the state for routes ensures that web doesn't encounter errors while going back
+      if (navigation.getState()?.routes) {
+        navigation.dispatch(StackActions.push(...router.matchPath('/')))
+      } else {
+        navigation.navigate('HomeTab')
+        navigation.dispatch(StackActions.popToTop())
+      }
+    }
+  }, [navigation, canGoBack])
+
+  return (
+    <CenteredView
+      style={[
+        a.flex_1,
+        a.align_center,
+        !gtMobile ? a.justify_between : a.gap_5xl,
+        t.atoms.border_contrast_low,
+        {paddingTop: 175, paddingBottom: 110},
+      ]}
+      sideBorders>
+      <View style={[a.w_full, a.align_center, a.gap_lg]}>
+        <Text style={[a.font_bold, a.text_3xl]}>{title}</Text>
+        <Text
+          style={[
+            a.text_md,
+            a.text_center,
+            t.atoms.text_contrast_high,
+            {lineHeight: 1.4},
+            gtMobile && {width: 450},
+          ]}>
+          {message}
+        </Text>
+      </View>
+      <View style={[a.gap_md, gtMobile ? {width: 350} : [a.w_full, a.px_lg]]}>
+        {onRetry && (
+          <Button
+            variant="solid"
+            color="primary"
+            label="Click here"
+            onPress={onRetry}
+            size="large"
+            style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
+            Retry
+          </Button>
+        )}
+        <Button
+          variant="solid"
+          color={onRetry ? 'secondary' : 'primary'}
+          label="Click here"
+          onPress={onGoBack}
+          size="large"
+          style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
+          Go Back
+        </Button>
+      </View>
+    </CenteredView>
+  )
+}
diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx
index 6d0613511..f924f0f59 100644
--- a/src/components/LabelingServiceCard/index.tsx
+++ b/src/components/LabelingServiceCard/index.tsx
@@ -104,7 +104,7 @@ export function Default({
 }: LabelingServiceProps & ViewStyleProp) {
   return (
     <Outer style={style}>
-      <Avatar />
+      <Avatar avatar={labeler.creator.avatar} />
       <Content>
         <Title
           value={getLabelingServiceTitle({
diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx
index 8a889c15e..8e4a58007 100644
--- a/src/components/Lists.tsx
+++ b/src/components/Lists.tsx
@@ -1,26 +1,28 @@
 import React from 'react'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {View} from 'react-native'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
+
 import {CenteredView} from 'view/com/util/Views'
 import {Loader} from '#/components/Loader'
-import {Trans} from '@lingui/macro'
 import {cleanError} from 'lib/strings/errors'
 import {Button} from '#/components/Button'
 import {Text} from '#/components/Typography'
-import {StackActions} from '@react-navigation/native'
-import {router} from '#/routes'
-import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
+import {Error} from '#/components/Error'
 
 export function ListFooter({
   isFetching,
   isError,
   error,
   onRetry,
+  height,
 }: {
-  isFetching: boolean
-  isError: boolean
+  isFetching?: boolean
+  isError?: boolean
   error?: string
   onRetry?: () => Promise<unknown>
+  height?: number
 }) {
   const t = useTheme()
 
@@ -29,11 +31,10 @@ export function ListFooter({
       style={[
         a.w_full,
         a.align_center,
-        a.justify_center,
         a.border_t,
         a.pb_lg,
         t.atoms.border_contrast_low,
-        {height: 180},
+        {height: height ?? 180, paddingTop: 30},
       ]}>
       {isFetching ? (
         <Loader size="xl" />
@@ -53,11 +54,12 @@ function ListFooterMaybeError({
   error,
   onRetry,
 }: {
-  isError: boolean
+  isError?: boolean
   error?: string
   onRetry?: () => Promise<unknown>
 }) {
   const t = useTheme()
+  const {_} = useLingui()
 
   if (!isError) return null
 
@@ -83,7 +85,7 @@ function ListFooterMaybeError({
         </Text>
         <Button
           variant="gradient"
-          label="Press to retry"
+          label={_(msg`Press to retry`)}
           style={[
             a.align_center,
             a.justify_center,
@@ -93,7 +95,7 @@ function ListFooterMaybeError({
             a.py_sm,
           ]}
           onPress={onRetry}>
-          Retry
+          <Trans>Retry</Trans>
         </Button>
       </View>
     </View>
@@ -128,121 +130,72 @@ export function ListMaybePlaceholder({
   isLoading,
   isEmpty,
   isError,
-  empty,
-  error,
-  notFoundType = 'page',
+  emptyTitle,
+  emptyMessage,
+  errorTitle,
+  errorMessage,
+  emptyType = 'page',
   onRetry,
 }: {
   isLoading: boolean
-  isEmpty: boolean
-  isError: boolean
-  empty?: string
-  error?: string
-  notFoundType?: 'page' | 'results'
+  isEmpty?: boolean
+  isError?: boolean
+  emptyTitle?: string
+  emptyMessage?: string
+  errorTitle?: string
+  errorMessage?: string
+  emptyType?: 'page' | 'results'
   onRetry?: () => Promise<unknown>
 }) {
-  const navigation = useNavigationDeduped()
   const t = useTheme()
+  const {_} = useLingui()
   const {gtMobile, gtTablet} = useBreakpoints()
+  const {_} = useLingui()
 
-  const canGoBack = navigation.canGoBack()
-  const onGoBack = React.useCallback(() => {
-    if (canGoBack) {
-      navigation.goBack()
-    } else {
-      navigation.navigate('HomeTab')
-
-      // Checking the state for routes ensures that web doesn't encounter errors while going back
-      if (navigation.getState()?.routes) {
-        navigation.dispatch(StackActions.push(...router.matchPath('/')))
-      } else {
-        navigation.navigate('HomeTab')
-        navigation.dispatch(StackActions.popToTop())
-      }
-    }
-  }, [navigation, canGoBack])
+  if (!isLoading && isError) {
+    return (
+      <Error
+        title={errorTitle ?? _(msg`Oops!`)}
+        message={errorMessage ?? _(`Something went wrong!`)}
+        onRetry={onRetry}
+      />
+    )
+  }
 
-  if (!isEmpty) return null
-
-  return (
-    <CenteredView
-      style={[
-        a.flex_1,
-        a.align_center,
-        !gtMobile ? a.justify_between : a.gap_5xl,
-        t.atoms.border_contrast_low,
-        {paddingTop: 175, paddingBottom: 110},
-      ]}
-      sideBorders={gtMobile}
-      topBorder={!gtTablet}>
-      {isLoading ? (
+  if (isLoading) {
+    return (
+      <CenteredView
+        style={[
+          a.flex_1,
+          a.align_center,
+          !gtMobile ? a.justify_between : a.gap_5xl,
+          t.atoms.border_contrast_low,
+          {paddingTop: 175, paddingBottom: 110},
+        ]}
+        sideBorders={gtMobile}
+        topBorder={!gtTablet}>
         <View style={[a.w_full, a.align_center, {top: 100}]}>
           <Loader size="xl" />
         </View>
-      ) : (
-        <>
-          <View style={[a.w_full, a.align_center, a.gap_lg]}>
-            <Text style={[a.font_bold, a.text_3xl]}>
-              {isError ? (
-                <Trans>Oops!</Trans>
-              ) : isEmpty ? (
-                <>
-                  {notFoundType === 'results' ? (
-                    <Trans>No results found</Trans>
-                  ) : (
-                    <Trans>Page not found</Trans>
-                  )}
-                </>
-              ) : undefined}
-            </Text>
+      </CenteredView>
+    )
+  }
 
-            {isError ? (
-              <Text
-                style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
-                {error ? error : <Trans>Something went wrong!</Trans>}
-              </Text>
-            ) : isEmpty ? (
-              <Text
-                style={[a.text_md, a.text_center, t.atoms.text_contrast_high]}>
-                {empty ? (
-                  empty
-                ) : (
-                  <Trans>
-                    We're sorry! We can't find the page you were looking for.
-                  </Trans>
-                )}
-              </Text>
-            ) : undefined}
-          </View>
-          <View
-            style={[a.gap_md, !gtMobile ? [a.w_full, a.px_lg] : {width: 350}]}>
-            {isError && onRetry && (
-              <Button
-                variant="solid"
-                color="primary"
-                label="Click here"
-                onPress={onRetry}
-                size="large"
-                style={[
-                  a.rounded_sm,
-                  a.overflow_hidden,
-                  {paddingVertical: 10},
-                ]}>
-                Retry
-              </Button>
-            )}
-            <Button
-              variant="solid"
-              color={isError && onRetry ? 'secondary' : 'primary'}
-              label="Click here"
-              onPress={onGoBack}
-              size="large"
-              style={[a.rounded_sm, a.overflow_hidden, {paddingVertical: 10}]}>
-              Go Back
-            </Button>
-          </View>
-        </>
-      )}
-    </CenteredView>
-  )
+  if (isEmpty) {
+    return (
+      <Error
+        title={
+          emptyTitle ??
+          (emptyType === 'results'
+            ? _(msg`No results found`)
+            : _(msg`Page not found`))
+        }
+        message={
+          emptyMessage ??
+          _(msg`We're sorry! We can't find the page you were looking for.`)
+        }
+        onRetry={onRetry}
+      />
+    )
+  }
 }
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index 9be9dd86b..051e95b95 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -17,7 +17,7 @@ import {
   ItemIconProps,
 } from '#/components/Menu/types'
 import {Button, ButtonText} from '#/components/Button'
-import {msg} from '@lingui/macro'
+import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {isNative} from 'platform/detection'
 
@@ -209,7 +209,9 @@ function Cancel() {
       variant="ghost"
       color="secondary"
       onPress={() => control.close()}>
-      <ButtonText>Cancel</ButtonText>
+      <ButtonText>
+        <Trans>Cancel</Trans>
+      </ButtonText>
     </Button>
   )
 }
diff --git a/src/components/ReportDialog/SelectLabelerView.tsx b/src/components/ReportDialog/SelectLabelerView.tsx
index 300114fc4..817426355 100644
--- a/src/components/ReportDialog/SelectLabelerView.tsx
+++ b/src/components/ReportDialog/SelectLabelerView.tsx
@@ -5,12 +5,13 @@ import {useLingui} from '@lingui/react'
 import {AppBskyLabelerDefs} from '@atproto/api'
 
 export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
+import {getLabelingServiceTitle} from '#/lib/moderation'
 
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, useTheme, useBreakpoints} from '#/alf'
 import {Text} from '#/components/Typography'
 import {Button, useButtonContext} from '#/components/Button'
 import {Divider} from '#/components/Divider'
-import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
+import * as LabelingServiceCard from '#/components/LabelingServiceCard'
 
 import {ReportDialogProps} from './types'
 
@@ -22,31 +23,29 @@ export function SelectLabelerView({
 }) {
   const t = useTheme()
   const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
 
   return (
     <View style={[a.gap_lg]}>
-      <View style={[a.justify_center, a.gap_sm]}>
+      <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
         <Text style={[a.text_2xl, a.font_bold]}>
-          <Trans>Select moderation service</Trans>
+          <Trans>Select moderator</Trans>
         </Text>
         <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
-          <Trans>Who do you want to send this report to?</Trans>
+          <Trans>To whom would you like to send this report?</Trans>
         </Text>
       </View>
 
       <Divider />
 
-      <View style={[a.gap_sm, {marginHorizontal: a.p_md.padding * -1}]}>
+      <View style={[a.gap_xs, {marginHorizontal: a.p_md.padding * -1}]}>
         {props.labelers.map(labeler => {
           return (
             <Button
               key={labeler.creator.did}
               label={_(msg`Send report to ${labeler.creator.displayName}`)}
               onPress={() => props.onSelectLabeler(labeler.creator.did)}>
-              <LabelerButton
-                title={labeler.creator.displayName || labeler.creator.handle}
-                description={labeler.creator.description || ''}
-              />
+              <LabelerButton labeler={labeler} />
             </Button>
           )
         })}
@@ -56,11 +55,9 @@ export function SelectLabelerView({
 }
 
 function LabelerButton({
-  title,
-  description,
+  labeler,
 }: {
-  title: string
-  description: string
+  labeler: AppBskyLabelerDefs.LabelerViewDetailed
 }) {
   const t = useTheme()
   const {hovered, pressed} = useButtonContext()
@@ -75,41 +72,21 @@ function LabelerButton({
   }, [t])
 
   return (
-    <View
-      style={[
-        a.w_full,
-        a.flex_row,
-        a.align_center,
-        a.justify_between,
-        a.p_md,
-        a.rounded_md,
-        {paddingRight: 70},
-        interacted && styles.interacted,
-      ]}>
-      <View style={[a.flex_1, a.gap_xs]}>
-        <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}>
-          {title}
-        </Text>
-        <Text style={[a.leading_tight, {maxWidth: 400}]} numberOfLines={3}>
-          {description}
-        </Text>
-      </View>
-
-      <View
-        style={[
-          a.absolute,
-          a.inset_0,
-          a.justify_center,
-          a.pr_md,
-          {left: 'auto'},
-        ]}>
-        <ChevronRight
-          size="md"
-          fill={
-            hovered ? t.palette.primary_500 : t.atoms.text_contrast_low.color
-          }
+    <LabelingServiceCard.Outer
+      style={[a.p_md, a.rounded_sm, interacted && styles.interacted]}>
+      <LabelingServiceCard.Avatar avatar={labeler.creator.avatar} />
+      <LabelingServiceCard.Content>
+        <LabelingServiceCard.Title
+          value={getLabelingServiceTitle({
+            displayName: labeler.creator.displayName,
+            handle: labeler.creator.handle,
+          })}
         />
-      </View>
-    </View>
+        <Text
+          style={[t.atoms.text_contrast_medium, a.text_sm, a.font_semibold]}>
+          @{labeler.creator.handle}
+        </Text>
+      </LabelingServiceCard.Content>
+    </LabelingServiceCard.Outer>
   )
 }
diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx
index 037a62fce..bacf5a867 100644
--- a/src/components/ReportDialog/SelectReportOptionView.tsx
+++ b/src/components/ReportDialog/SelectReportOptionView.tsx
@@ -9,7 +9,7 @@ import {DMCA_LINK} from '#/components/ReportDialog/const'
 import {Link} from '#/components/Link'
 export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
 
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, useTheme, useBreakpoints} from '#/alf'
 import {Text} from '#/components/Typography'
 import {
   Button,
@@ -35,6 +35,7 @@ export function SelectReportOptionView({
 }) {
   const t = useTheme()
   const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
   const allReportOptions = useReportOptions()
   const reportOptions = allReportOptions[props.params.type]
 
@@ -76,7 +77,7 @@ export function SelectReportOptionView({
         </Button>
       ) : null}
 
-      <View style={[a.justify_center, a.gap_sm]}>
+      <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
         <Text style={[a.text_2xl, a.font_bold]}>{i18n.title}</Text>
         <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
           {i18n.description}
diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx
index 13e4e7d6b..0ed703667 100644
--- a/src/components/TagMenu/index.tsx
+++ b/src/components/TagMenu/index.tsx
@@ -264,7 +264,9 @@ export function TagMenu({
                 variant="ghost"
                 color="secondary"
                 onPress={() => control.close()}>
-                <ButtonText>Cancel</ButtonText>
+                <ButtonText>
+                  <Trans>Cancel</Trans>
+                </ButtonText>
               </Button>
             </>
           )}
diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx
index 62c95c78d..4a3e96e56 100644
--- a/src/components/dialogs/BirthDateSettings.tsx
+++ b/src/components/dialogs/BirthDateSettings.tsx
@@ -1,48 +1,64 @@
 import React from 'react'
 import {useLingui} from '@lingui/react'
 import {Trans, msg} from '@lingui/macro'
+import {View} from 'react-native'
 
 import * as Dialog from '#/components/Dialog'
 import {Text} from '../Typography'
 import {DateInput} from '#/view/com/util/forms/DateInput'
 import {logger} from '#/logger'
 import {
+  usePreferencesQuery,
   usePreferencesSetBirthDateMutation,
   UsePreferencesQueryResponse,
 } from '#/state/queries/preferences'
-import {Button, ButtonText} from '../Button'
+import {Button, ButtonIcon, ButtonText} from '../Button'
 import {atoms as a, useTheme} from '#/alf'
 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
 import {cleanError} from '#/lib/strings/errors'
-import {ActivityIndicator, View} from 'react-native'
 import {isIOS, isWeb} from '#/platform/detection'
+import {Loader} from '#/components/Loader'
 
 export function BirthDateSettingsDialog({
   control,
-  preferences,
 }: {
   control: Dialog.DialogControlProps
-  preferences: UsePreferencesQueryResponse | undefined
 }) {
+  const t = useTheme()
   const {_} = useLingui()
-  const {isPending, isError, error, mutateAsync} =
-    usePreferencesSetBirthDateMutation()
+  const {isLoading, error, data: preferences} = usePreferencesQuery()
 
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
+
       <Dialog.ScrollableInner label={_(msg`My Birthday`)}>
-        {preferences && !isPending ? (
-          <BirthdayInner
-            control={control}
-            preferences={preferences}
-            isError={isError}
-            error={error}
-            setBirthDate={mutateAsync}
+        <View style={[a.gap_sm, a.pb_lg]}>
+          <Text style={[a.text_2xl, a.font_bold]}>
+            <Trans>My Birthday</Trans>
+          </Text>
+          <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+            <Trans>This information is not shared with other users.</Trans>
+          </Text>
+        </View>
+
+        {isLoading ? (
+          <Loader size="xl" />
+        ) : error || !preferences ? (
+          <ErrorMessage
+            message={
+              error?.toString() ||
+              _(
+                msg`We were unable to load your birth date preferences. Please try again.`,
+              )
+            }
+            style={[a.rounded_sm]}
           />
         ) : (
-          <ActivityIndicator size="large" style={a.my_5xl} />
+          <BirthdayInner control={control} preferences={preferences} />
         )}
+
+        <Dialog.Close />
       </Dialog.ScrollableInner>
     </Dialog.Outer>
   )
@@ -51,20 +67,18 @@ export function BirthDateSettingsDialog({
 function BirthdayInner({
   control,
   preferences,
-  isError,
-  error,
-  setBirthDate,
 }: {
   control: Dialog.DialogControlProps
   preferences: UsePreferencesQueryResponse
-  isError: boolean
-  error: unknown
-  setBirthDate: (args: {birthDate: Date}) => Promise<unknown>
 }) {
   const {_} = useLingui()
   const [date, setDate] = React.useState(preferences.birthDate || new Date())
-  const t = useTheme()
-
+  const {
+    isPending,
+    isError,
+    error,
+    mutateAsync: setBirthDate,
+  } = usePreferencesSetBirthDateMutation()
   const hasChanged = date !== preferences.birthDate
 
   const onSave = React.useCallback(async () => {
@@ -74,21 +88,13 @@ function BirthdayInner({
         await setBirthDate({birthDate: date})
       }
       control.close()
-    } catch (e) {
-      logger.error(`setBirthDate failed`, {message: e})
+    } catch (e: any) {
+      logger.error(`setBirthDate failed`, {message: e.message})
     }
   }, [date, setBirthDate, control, hasChanged])
 
   return (
     <View style={a.gap_lg} testID="birthDateSettingsDialog">
-      <View style={[a.gap_sm]}>
-        <Text style={[a.text_2xl, a.font_bold]}>
-          <Trans>My Birthday</Trans>
-        </Text>
-        <Text style={t.atoms.text_contrast_medium}>
-          <Trans>This information is not shared with other users.</Trans>
-        </Text>
-      </View>
       <View style={isIOS && [a.w_full, a.align_center]}>
         <DateInput
           handleAsUTC
@@ -103,6 +109,7 @@ function BirthdayInner({
           accessibilityLabelledBy="birthDate"
         />
       </View>
+
       {isError ? (
         <ErrorMessage message={cleanError(error)} style={[a.rounded_sm]} />
       ) : undefined}
@@ -110,13 +117,14 @@ function BirthdayInner({
       <View style={isWeb && [a.flex_row, a.justify_end]}>
         <Button
           label={hasChanged ? _(msg`Save birthday`) : _(msg`Done`)}
-          size={isWeb ? 'small' : 'medium'}
+          size="medium"
           onPress={onSave}
           variant="solid"
           color="primary">
           <ButtonText>
             {hasChanged ? <Trans>Save</Trans> : <Trans>Done</Trans>}
           </ButtonText>
+          {isPending && <ButtonIcon icon={Loader} />}
         </Button>
       </View>
     </View>
diff --git a/src/components/moderation/ModerationLabelPref.tsx b/src/components/moderation/ModerationLabelPref.tsx
index f14550488..b16c24859 100644
--- a/src/components/moderation/ModerationLabelPref.tsx
+++ b/src/components/moderation/ModerationLabelPref.tsx
@@ -12,7 +12,7 @@ import {
 } from '#/state/queries/preferences'
 import {getLabelStrings} from '#/lib/moderation/useLabelInfo'
 
-import {useTheme, atoms as a} from '#/alf'
+import {useTheme, atoms as a, useBreakpoints} from '#/alf'
 import {Text} from '#/components/Typography'
 import {InlineLink} from '#/components/Link'
 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
@@ -29,6 +29,7 @@ export function ModerationLabelPref({
 }) {
   const {_, i18n} = useLingui()
   const t = useTheme()
+  const {gtPhone} = useBreakpoints()
 
   const isGlobalLabel = !labelValueDefinition.definedBy
   const {identifier} = labelValueDefinition
@@ -57,6 +58,7 @@ export function ModerationLabelPref({
     adultOnly && !preferences?.moderationPrefs.adultContentEnabled
   // are there any reasons we cant configure this label here?
   const cantConfigure = isGlobalLabel || adultDisabled
+  const showConfig = !disabled && (gtPhone || !cantConfigure)
 
   // adjust the pref based on whether warn is available
   let prefAdjusted = pref
@@ -85,9 +87,19 @@ export function ModerationLabelPref({
   )
 
   return (
-    <View style={[a.flex_row, a.gap_sm, a.px_lg, a.py_lg, a.justify_between]}>
+    <View
+      style={[
+        a.flex_row,
+        a.gap_md,
+        a.px_lg,
+        a.py_lg,
+        a.justify_between,
+        a.flex_wrap,
+      ]}>
       <View style={[a.gap_xs, a.flex_1]}>
-        <Text style={[a.font_bold]}>{labelStrings.name}</Text>
+        <Text style={[a.font_bold, gtPhone ? a.text_sm : a.text_md]}>
+          {labelStrings.name}
+        </Text>
         <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
           {labelStrings.description}
         </Text>
@@ -113,40 +125,51 @@ export function ModerationLabelPref({
           </View>
         )}
       </View>
-      {disabled ? (
-        <></>
-      ) : cantConfigure ? (
-        <View style={[{minHeight: 35}, a.px_sm, a.py_md]}>
-          <Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
-            {currentPrefLabel}
-          </Text>
-        </View>
-      ) : (
-        <View style={[{minHeight: 35}]}>
-          <ToggleButton.Group
-            label={_(
-              msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`,
-            )}
-            values={[prefAdjusted]}
-            onChange={newPref =>
-              mutate({
-                label: identifier,
-                visibility: newPref[0] as LabelPreference,
-                labelerDid,
-              })
-            }>
-            <ToggleButton.Button name="ignore" label={ignoreLabel}>
-              {ignoreLabel}
-            </ToggleButton.Button>
-            {canWarn && (
-              <ToggleButton.Button name="warn" label={warnLabel}>
-                {warnLabel}
-              </ToggleButton.Button>
-            )}
-            <ToggleButton.Button name="hide" label={hideLabel}>
-              {hideLabel}
-            </ToggleButton.Button>
-          </ToggleButton.Group>
+
+      {showConfig && (
+        <View style={[gtPhone ? undefined : a.w_full]}>
+          {cantConfigure ? (
+            <View
+              style={[
+                {minHeight: 35},
+                a.px_md,
+                a.py_md,
+                a.rounded_sm,
+                a.border,
+                t.atoms.border_contrast_low,
+              ]}>
+              <Text style={[a.font_bold, t.atoms.text_contrast_low]}>
+                {currentPrefLabel}
+              </Text>
+            </View>
+          ) : (
+            <View style={[{minHeight: 35}]}>
+              <ToggleButton.Group
+                label={_(
+                  msg`Configure content filtering setting for category: ${labelStrings.name.toLowerCase()}`,
+                )}
+                values={[prefAdjusted]}
+                onChange={newPref =>
+                  mutate({
+                    label: identifier,
+                    visibility: newPref[0] as LabelPreference,
+                    labelerDid,
+                  })
+                }>
+                <ToggleButton.Button name="ignore" label={ignoreLabel}>
+                  {ignoreLabel}
+                </ToggleButton.Button>
+                {canWarn && (
+                  <ToggleButton.Button name="warn" label={warnLabel}>
+                    {warnLabel}
+                  </ToggleButton.Button>
+                )}
+                <ToggleButton.Button name="hide" label={hideLabel}>
+                  {hideLabel}
+                </ToggleButton.Button>
+              </ToggleButton.Group>
+            </View>
+          )}
         </View>
       )}
     </View>