about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/AppPasswords.tsx13
-rw-r--r--src/view/screens/Debug.tsx7
-rw-r--r--src/view/screens/Feeds.tsx12
-rw-r--r--src/view/screens/Home.tsx4
-rw-r--r--src/view/screens/Lists.tsx4
-rw-r--r--src/view/screens/Log.tsx4
-rw-r--r--src/view/screens/Moderation.tsx8
-rw-r--r--src/view/screens/ModerationModlists.tsx2
-rw-r--r--src/view/screens/PostThread.tsx9
-rw-r--r--src/view/screens/PreferencesExternalEmbeds.tsx138
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx19
-rw-r--r--src/view/screens/PreferencesThreads.tsx20
-rw-r--r--src/view/screens/Profile.tsx7
-rw-r--r--src/view/screens/ProfileFeed.tsx62
-rw-r--r--src/view/screens/ProfileList.tsx96
-rw-r--r--src/view/screens/SavedFeeds.tsx27
-rw-r--r--src/view/screens/Search/Search.tsx315
-rw-r--r--src/view/screens/Search/index.tsx4
-rw-r--r--src/view/screens/Search/index.web.tsx3
-rw-r--r--src/view/screens/Settings.tsx168
-rw-r--r--src/view/screens/Storybook/Breakpoints.tsx25
-rw-r--r--src/view/screens/Storybook/Buttons.tsx124
-rw-r--r--src/view/screens/Storybook/Dialogs.tsx90
-rw-r--r--src/view/screens/Storybook/Forms.tsx215
-rw-r--r--src/view/screens/Storybook/Icons.tsx41
-rw-r--r--src/view/screens/Storybook/Links.tsx48
-rw-r--r--src/view/screens/Storybook/Palette.tsx336
-rw-r--r--src/view/screens/Storybook/Shadows.tsx53
-rw-r--r--src/view/screens/Storybook/Spacing.tsx64
-rw-r--r--src/view/screens/Storybook/Theming.tsx56
-rw-r--r--src/view/screens/Storybook/Typography.tsx30
-rw-r--r--src/view/screens/Storybook/index.tsx78
-rw-r--r--src/view/screens/Support.tsx4
33 files changed, 1872 insertions, 214 deletions
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index 154035f22..dc439c367 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -33,6 +33,7 @@ import {cleanError} from '#/lib/strings/errors'
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
 export function AppPasswords({}: Props) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
   const {isTabletOrDesktop} = useWebMediaQueries()
@@ -61,8 +62,8 @@ export function AppPasswords({}: Props) {
         ]}
         testID="appPasswordsScreen">
         <ErrorScreen
-          title="Oops!"
-          message="There was an issue with fetching your app passwords"
+          title={_(msg`Oops!`)}
+          message={_(msg`There was an issue with fetching your app passwords`)}
           details={cleanError(error)}
         />
       </CenteredView>
@@ -98,7 +99,7 @@ export function AppPasswords({}: Props) {
           <Button
             testID="appPasswordBtn"
             type="primary"
-            label="Add App Password"
+            label={_(msg`Add App Password`)}
             style={styles.btn}
             labelStyle={styles.btnLabel}
             onPress={onAdd}
@@ -139,7 +140,7 @@ export function AppPasswords({}: Props) {
               <Button
                 testID="appPasswordBtn"
                 type="primary"
-                label="Add App Password"
+                label={_(msg`Add App Password`)}
                 style={styles.btn}
                 labelStyle={styles.btnLabel}
                 onPress={onAdd}
@@ -152,7 +153,7 @@ export function AppPasswords({}: Props) {
             <Button
               testID="appPasswordBtn"
               type="primary"
-              label="Add App Password"
+              label={_(msg`Add App Password`)}
               style={styles.btn}
               labelStyle={styles.btnLabel}
               onPress={onAdd}
@@ -224,7 +225,7 @@ function AppPassword({
       ),
       async onPressConfirm() {
         await deleteMutation.mutateAsync({name})
-        Toast.show('App password deleted')
+        Toast.show(_(msg`App password deleted`))
       },
     })
   }, [deleteMutation, openModal, name, _])
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index 0e0464200..f26b1505a 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -16,6 +16,8 @@ import {ToggleButton} from '../com/util/forms/ToggleButton'
 import {RadioGroup} from '../com/util/forms/RadioGroup'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {ErrorMessage} from '../com/util/error/ErrorMessage'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs']
 
@@ -48,6 +50,7 @@ function DebugInner({
 }) {
   const [currentView, setCurrentView] = React.useState<number>(0)
   const pal = usePalette('default')
+  const {_} = useLingui()
 
   const renderItem = (item: any) => {
     return (
@@ -57,7 +60,7 @@ function DebugInner({
             type="default-light"
             onPress={onToggleColorScheme}
             isSelected={colorScheme === 'dark'}
-            label="Dark mode"
+            label={_(msg`Dark mode`)}
           />
         </View>
         {item.currentView === 3 ? (
@@ -77,7 +80,7 @@ function DebugInner({
 
   return (
     <View style={[s.hContentRegion, pal.view]}>
-      <ViewHeader title="Debug panel" />
+      <ViewHeader title={_(msg`Debug panel`)} />
       <ViewSelector
         swipeEnabled
         sections={MAIN_VIEWS}
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 20cdf815a..a913364d4 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -97,6 +97,7 @@ export function FeedsScreen(_props: Props) {
     data: preferences,
     isLoading: isPreferencesLoading,
     error: preferencesError,
+    refetch: refetchPreferences,
   } = usePreferencesQuery()
   const {
     data: popularFeeds,
@@ -151,9 +152,12 @@ export function FeedsScreen(_props: Props) {
   }, [query, debouncedSearch])
   const onPullToRefresh = React.useCallback(async () => {
     setIsPTR(true)
-    await refetchPopularFeeds()
+    await Promise.all([
+      refetchPreferences().catch(_e => undefined),
+      refetchPopularFeeds().catch(_e => undefined),
+    ])
     setIsPTR(false)
-  }, [setIsPTR, refetchPopularFeeds])
+  }, [setIsPTR, refetchPreferences, refetchPopularFeeds])
   const onEndReached = React.useCallback(() => {
     if (
       isPopularFeedsFetching ||
@@ -328,7 +332,7 @@ export function FeedsScreen(_props: Props) {
         hitSlop={10}
         accessibilityRole="button"
         accessibilityLabel={_(msg`Edit Saved Feeds`)}
-        accessibilityHint="Opens screen to edit Saved Feeds">
+        accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
         <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
       </Link>
     )
@@ -494,6 +498,8 @@ export function FeedsScreen(_props: Props) {
         // @ts-ignore our .web version only -prf
         desktopFixedHeight
         scrollIndicatorInsets={{right: 1}}
+        keyboardShouldPersistTaps="handled"
+        keyboardDismissMode="on-drag"
       />
 
       {hasSession && (
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index b8033f0b4..7d6a40f02 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -109,7 +109,9 @@ function HomeScreenReady({
   const homeFeedParams = React.useMemo<FeedParams>(() => {
     return {
       mergeFeedEnabled: Boolean(preferences.feedViewPrefs.lab_mergeFeedEnabled),
-      mergeFeedSources: preferences.feeds.saved,
+      mergeFeedSources: preferences.feedViewPrefs.lab_mergeFeedEnabled
+        ? preferences.feeds.saved
+        : [],
     }
   }, [preferences])
 
diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx
index d28db7c6c..bdd5dd9b7 100644
--- a/src/view/screens/Lists.tsx
+++ b/src/view/screens/Lists.tsx
@@ -61,7 +61,7 @@ export function ListsScreen({}: Props) {
             <Trans>Public, shareable lists which can drive feeds.</Trans>
           </Text>
         </View>
-        <View>
+        <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
           <Button
             testID="newUserListBtn"
             type="default"
@@ -73,7 +73,7 @@ export function ListsScreen({}: Props) {
             }}>
             <FontAwesomeIcon icon="plus" color={pal.colors.text} />
             <Text type="button" style={pal.text}>
-              <Trans>New</Trans>
+              <Trans context="action">New</Trans>
             </Text>
           </Button>
         </View>
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
index 8680b851b..e727a1fb8 100644
--- a/src/view/screens/Log.tsx
+++ b/src/view/screens/Log.tsx
@@ -50,7 +50,9 @@ export function LogScreen({}: NativeStackScreenProps<
                   style={[styles.entry, pal.border, pal.view]}
                   onPress={toggler(entry.id)}
                   accessibilityLabel={_(msg`View debug entry`)}
-                  accessibilityHint="Opens additional details for a debug entry">
+                  accessibilityHint={_(
+                    msg`Opens additional details for a debug entry`,
+                  )}>
                   {entry.level === 'debug' ? (
                     <FontAwesomeIcon icon="info" />
                   ) : (
diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx
index 1bf8db2e0..96bb46cef 100644
--- a/src/view/screens/Moderation.tsx
+++ b/src/view/screens/Moderation.tsx
@@ -62,7 +62,7 @@ export function ModerationScreen({}: Props) {
       ]}
       testID="moderationScreen">
       <ViewHeader title={_(msg`Moderation`)} showOnDesktop />
-      <ScrollView>
+      <ScrollView contentContainerStyle={[styles.noBorder]}>
         <View style={styles.spacer} />
         <TouchableOpacity
           testID="contentFilteringBtn"
@@ -275,4 +275,10 @@ const styles = StyleSheet.create({
     borderRadius: 30,
     marginRight: 12,
   },
+  noBorder: {
+    borderBottomWidth: 0,
+    borderRightWidth: 0,
+    borderLeftWidth: 0,
+    borderTopWidth: 0,
+  },
 })
diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx
index d6a3b5f6f..b7d993acc 100644
--- a/src/view/screens/ModerationModlists.tsx
+++ b/src/view/screens/ModerationModlists.tsx
@@ -63,7 +63,7 @@ export function ModerationModlistsScreen({}: Props) {
             </Trans>
           </Text>
         </View>
-        <View>
+        <View style={[{marginLeft: 18}, isMobile && {marginLeft: 12}]}>
           <Button
             testID="newModListBtn"
             type="default"
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 9f50c8b73..276dc842c 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -25,6 +25,7 @@ import {ErrorMessage} from '../com/util/error/ErrorMessage'
 import {CenteredView} from '../com/util/Views'
 import {useComposerControls} from '#/state/shell/composer'
 import {useSession} from '#/state/session'
+import {isWeb} from '#/platform/detection'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
 export function PostThreadScreen({route}: Props) {
@@ -67,6 +68,7 @@ export function PostThreadScreen({route}: Props) {
           displayName: thread.post.author.displayName,
           avatar: thread.post.author.avatar,
         },
+        embed: thread.post.embed,
       },
       onPost: () =>
         queryClient.invalidateQueries({
@@ -77,7 +79,9 @@ export function PostThreadScreen({route}: Props) {
 
   return (
     <View style={s.hContentRegion}>
-      {isMobile && <ViewHeader title={_(msg`Post`)} />}
+      {isMobile && (
+        <ViewHeader title={_(msg({message: 'Post', context: 'description'}))} />
+      )}
       <View style={s.flex1}>
         {uriError ? (
           <CenteredView>
@@ -109,7 +113,8 @@ export function PostThreadScreen({route}: Props) {
 
 const styles = StyleSheet.create({
   prompt: {
-    position: 'absolute',
+    // @ts-ignore web-only
+    position: isWeb ? 'fixed' : 'absolute',
     left: 0,
     right: 0,
   },
diff --git a/src/view/screens/PreferencesExternalEmbeds.tsx b/src/view/screens/PreferencesExternalEmbeds.tsx
new file mode 100644
index 000000000..1e8cedf7e
--- /dev/null
+++ b/src/view/screens/PreferencesExternalEmbeds.tsx
@@ -0,0 +1,138 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
+import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
+import {s} from 'lib/styles'
+import {Text} from '../com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {
+  EmbedPlayerSource,
+  externalEmbedLabels,
+} from '#/lib/strings/embed-player'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {Trans} from '@lingui/macro'
+import {ScrollView} from '../com/util/Views'
+import {
+  useExternalEmbedsPrefs,
+  useSetExternalEmbedPref,
+} from 'state/preferences'
+import {ToggleButton} from 'view/com/util/forms/ToggleButton'
+import {SimpleViewHeader} from '../com/util/SimpleViewHeader'
+
+type Props = NativeStackScreenProps<
+  CommonNavigatorParams,
+  'PreferencesExternalEmbeds'
+>
+export function PreferencesExternalEmbeds({}: Props) {
+  const pal = usePalette('default')
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {screen} = useAnalytics()
+  const {isMobile} = useWebMediaQueries()
+
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('PreferencesExternalEmbeds')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
+
+  return (
+    <View style={s.hContentRegion} testID="preferencesExternalEmbedsScreen">
+      <SimpleViewHeader
+        showBackButton={isMobile}
+        style={[
+          pal.border,
+          {borderBottomWidth: 1},
+          !isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
+        ]}>
+        <View style={{flex: 1}}>
+          <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
+            <Trans>External Media Preferences</Trans>
+          </Text>
+          <Text style={pal.textLight}>
+            <Trans>Customize media from external sites.</Trans>
+          </Text>
+        </View>
+      </SimpleViewHeader>
+      <ScrollView
+        // @ts-ignore web only -prf
+        dataSet={{'stable-gutters': 1}}
+        contentContainerStyle={[pal.viewLight, {paddingBottom: 200}]}>
+        <View style={[pal.view]}>
+          <View style={styles.infoCard}>
+            <Text style={pal.text}>
+              <Trans>
+                External media may allow websites to collect information about
+                you and your device. No information is sent or requested until
+                you press the "play" button.
+              </Trans>
+            </Text>
+          </View>
+        </View>
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Enable media players for</Trans>
+        </Text>
+        {Object.entries(externalEmbedLabels).map(([key, label]) => (
+          <PrefSelector
+            source={key as EmbedPlayerSource}
+            label={label}
+            key={key}
+          />
+        ))}
+      </ScrollView>
+    </View>
+  )
+}
+
+function PrefSelector({
+  source,
+  label,
+}: {
+  source: EmbedPlayerSource
+  label: string
+}) {
+  const pal = usePalette('default')
+  const setExternalEmbedPref = useSetExternalEmbedPref()
+  const sources = useExternalEmbedsPrefs()
+
+  return (
+    <View>
+      <View style={[pal.view, styles.toggleCard]}>
+        <ToggleButton
+          type="default-light"
+          label={label}
+          labelType="lg"
+          isSelected={sources?.[source] === 'show'}
+          onPress={() =>
+            setExternalEmbedPref(
+              source,
+              sources?.[source] === 'show' ? 'hide' : 'show',
+            )
+          }
+        />
+      </View>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  heading: {
+    paddingHorizontal: 18,
+    paddingTop: 14,
+    paddingBottom: 14,
+  },
+  spacer: {
+    height: 8,
+  },
+  infoCard: {
+    paddingHorizontal: 20,
+    paddingVertical: 14,
+  },
+  toggleCard: {
+    paddingVertical: 8,
+    paddingHorizontal: 6,
+    marginBottom: 1,
+  },
+})
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx
index 20ef72923..7ad870937 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -27,8 +27,10 @@ function RepliesThresholdInput({
   initialValue: number
 }) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const [value, setValue] = useState(initialValue)
   const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
+  const preValue = React.useRef(initialValue)
   const save = React.useMemo(
     () =>
       debounce(
@@ -46,7 +48,12 @@ function RepliesThresholdInput({
       <Slider
         value={value}
         onValueChange={(v: number | number[]) => {
-          const threshold = Math.floor(Array.isArray(v) ? v[0] : v)
+          let threshold = Array.isArray(v) ? v[0] : v
+          if (threshold > preValue.current) threshold = Math.floor(threshold)
+          else threshold = Math.ceil(threshold)
+
+          preValue.current = threshold
+
           setValue(threshold)
           save(threshold)
         }}
@@ -58,10 +65,12 @@ function RepliesThresholdInput({
       />
       <Text type="xs" style={pal.text}>
         {value === 0
-          ? `Show all replies`
-          : `Show replies with at least ${value} ${
-              value > 1 ? `likes` : `like`
-            }`}
+          ? _(msg`Show all replies`)
+          : _(
+              msg`Show replies with at least ${value} ${
+                value > 1 ? `likes` : `like`
+              }`,
+            )}
       </Text>
     </View>
   )
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 73d941932..321c67293 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -75,10 +75,16 @@ export function PreferencesThreads({navigation}: Props) {
                 <RadioGroup
                   type="default-light"
                   items={[
-                    {key: 'oldest', label: 'Oldest replies first'},
-                    {key: 'newest', label: 'Newest replies first'},
-                    {key: 'most-likes', label: 'Most-liked replies first'},
-                    {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
+                    {key: 'oldest', label: _(msg`Oldest replies first`)},
+                    {key: 'newest', label: _(msg`Newest replies first`)},
+                    {
+                      key: 'most-likes',
+                      label: _(msg`Most-liked replies first`),
+                    },
+                    {
+                      key: 'random',
+                      label: _(msg`Random (aka "Poster's Roulette")`),
+                    },
                   ]}
                   onSelect={key => setThreadViewPrefs({sort: key})}
                   initialSelection={preferences?.threadViewPrefs?.sort}
@@ -97,7 +103,7 @@ export function PreferencesThreads({navigation}: Props) {
               </Text>
               <ToggleButton
                 type="default-light"
-                label={prioritizeFollowedUsers ? 'Yes' : 'No'}
+                label={prioritizeFollowedUsers ? _(msg`Yes`) : _(msg`No`)}
                 isSelected={prioritizeFollowedUsers}
                 onPress={() =>
                   setThreadViewPrefs({
@@ -120,7 +126,7 @@ export function PreferencesThreads({navigation}: Props) {
               </Text>
               <ToggleButton
                 type="default-light"
-                label={treeViewEnabled ? 'Yes' : 'No'}
+                label={treeViewEnabled ? _(msg`Yes`) : _(msg`No`)}
                 isSelected={treeViewEnabled}
                 onPress={() =>
                   setThreadViewPrefs({
@@ -153,7 +159,7 @@ export function PreferencesThreads({navigation}: Props) {
           accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
           <Text style={[s.white, s.bold, s.f18]}>
-            <Trans>Done</Trans>
+            <Trans context="action">Done</Trans>
           </Text>
         </TouchableOpacity>
       </View>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 4558ae33d..7fc4d7a20 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -371,6 +371,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
     ref,
   ) {
+    const {_} = useLingui()
     const queryClient = useQueryClient()
     const [hasNew, setHasNew] = React.useState(false)
     const [isScrolledDown, setIsScrolledDown] = React.useState(false)
@@ -388,8 +389,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     }))
 
     const renderPostsEmpty = React.useCallback(() => {
-      return <EmptyState icon="feed" message="This feed is empty!" />
-    }, [])
+      return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
+    }, [_])
 
     return (
       <View>
@@ -408,7 +409,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
         {(isScrolledDown || hasNew) && (
           <LoadLatestBtn
             onPress={onScrollToTop}
-            label="Load new posts"
+            label={_(msg`Load new posts`)}
             showIndicator={hasNew}
           />
         )}
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 211306c0d..61282497c 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -214,11 +214,21 @@ export function ProfileFeedScreenInner({
       }
     } catch (err) {
       Toast.show(
-        'There was an an issue updating your feeds, please check your internet connection and try again.',
+        _(
+          msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
+        ),
       )
       logger.error('Failed up update feeds', {error: err})
     }
-  }, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed])
+  }, [
+    feedInfo,
+    isSaved,
+    saveFeed,
+    removeFeed,
+    resetSaveFeed,
+    resetRemoveFeed,
+    _,
+  ])
 
   const onTogglePinned = React.useCallback(async () => {
     try {
@@ -232,10 +242,10 @@ export function ProfileFeedScreenInner({
         resetPinFeed()
       }
     } catch (e) {
-      Toast.show('There was an issue contacting the server')
+      Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {error: e})
     }
-  }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed])
+  }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _])
 
   const onPressShare = React.useCallback(() => {
     const url = toShareUrl(feedInfo.route.href)
@@ -341,7 +351,7 @@ export function ProfileFeedScreenInner({
             <Button
               disabled={isSavePending || isRemovePending}
               type="default"
-              label={isSaved ? 'Unsave' : 'Save'}
+              label={isSaved ? _(msg`Unsave`) : _(msg`Save`)}
               onPress={onToggleSaved}
               style={styles.btn}
             />
@@ -349,7 +359,7 @@ export function ProfileFeedScreenInner({
               testID={isPinned ? 'unpinBtn' : 'pinBtn'}
               disabled={isPinPending || isUnpinPending}
               type={isPinned ? 'default' : 'inverted'}
-              label={isPinned ? 'Unpin' : 'Pin to home'}
+              label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
               onPress={onTogglePinned}
               style={styles.btn}
             />
@@ -444,6 +454,7 @@ interface FeedSectionProps {
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
+    const {_} = useLingui()
     const [hasNew, setHasNew] = React.useState(false)
     const [isScrolledDown, setIsScrolledDown] = React.useState(false)
     const queryClient = useQueryClient()
@@ -470,8 +481,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     }, [onScrollToTop, isScreenFocused])
 
     const renderPostsEmpty = useCallback(() => {
-      return <EmptyState icon="feed" message="This feed is empty!" />
-    }, [])
+      return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
+    }, [_])
 
     return (
       <View>
@@ -479,6 +490,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
           enabled={isFocused}
           feed={feed}
           pollInterval={60e3}
+          disablePoll={hasNew}
           scrollElRef={scrollElRef}
           onHasNew={setHasNew}
           onScrolledDownChange={setIsScrolledDown}
@@ -488,7 +500,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
         {(isScrolledDown || hasNew) && (
           <LoadLatestBtn
             onPress={onScrollToTop}
-            label="Load new posts"
+            label={_(msg`Load new posts`)}
             showIndicator={hasNew}
           />
         )}
@@ -542,11 +554,13 @@ function AboutSection({
       }
     } catch (err) {
       Toast.show(
-        'There was an an issue contacting the server, please check your internet connection and try again.',
+        _(
+          msg`There was an an issue contacting the server, please check your internet connection and try again.`,
+        ),
       )
       logger.error('Failed up toggle like', {error: err})
     }
-  }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track])
+  }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
 
   return (
     <ScrollView
@@ -597,24 +611,28 @@ function AboutSection({
           {typeof likeCount === 'number' && (
             <TextLink
               href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
-              text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`}
+              text={_(
+                msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
+              )}
               style={[pal.textLight, s.semiBold]}
             />
           )}
         </View>
         <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-          Created by{' '}
           {isOwner ? (
-            'you'
+            <Trans>Created by you</Trans>
           ) : (
-            <TextLink
-              text={sanitizeHandle(feedInfo.creatorHandle, '@')}
-              href={makeProfileLink({
-                did: feedInfo.creatorDid,
-                handle: feedInfo.creatorHandle,
-              })}
-              style={pal.textLight}
-            />
+            <Trans>
+              Created by{' '}
+              <TextLink
+                text={sanitizeHandle(feedInfo.creatorHandle, '@')}
+                href={makeProfileLink({
+                  did: feedInfo.creatorDid,
+                  handle: feedInfo.creatorHandle,
+                })}
+                style={pal.textLight}
+              />
+            </Trans>
           )}
         </Text>
       </View>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index c51758ae5..cb7962a9b 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -68,6 +68,7 @@ interface SectionRef {
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
 export function ProfileListScreen(props: Props) {
+  const {_} = useLingui()
   const {name: handleOrDid, rkey} = props.route.params
   const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
     AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
@@ -78,7 +79,9 @@ export function ProfileListScreen(props: Props) {
     return (
       <CenteredView>
         <ErrorScreen
-          error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`}
+          error={_(
+            msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
+          )}
         />
       </CenteredView>
     )
@@ -260,10 +263,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         await pinFeed({uri: list.uri})
       }
     } catch (e) {
-      Toast.show('There was an issue contacting the server')
+      Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {error: e})
     }
-  }, [list.uri, isPinned, pinFeed, unpinFeed])
+  }, [list.uri, isPinned, pinFeed, unpinFeed, _])
 
   const onSubscribeMute = useCallback(() => {
     openModal({
@@ -272,15 +275,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       message: _(
         msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
       ),
-      confirmBtnText: 'Mute this List',
+      confirmBtnText: _(msg`Mute this List`),
       async onPressConfirm() {
         try {
           await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
-          Toast.show('List muted')
+          Toast.show(_(msg`List muted`))
           track('Lists:Mute')
         } catch {
           Toast.show(
-            'There was an issue. Please check your internet connection and try again.',
+            _(
+              msg`There was an issue. Please check your internet connection and try again.`,
+            ),
           )
         }
       },
@@ -293,14 +298,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const onUnsubscribeMute = useCallback(async () => {
     try {
       await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
-      Toast.show('List unmuted')
+      Toast.show(_(msg`List unmuted`))
       track('Lists:Unmute')
     } catch {
       Toast.show(
-        'There was an issue. Please check your internet connection and try again.',
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
       )
     }
-  }, [list, listMuteMutation, track])
+  }, [list, listMuteMutation, track, _])
 
   const onSubscribeBlock = useCallback(() => {
     openModal({
@@ -309,15 +316,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       message: _(
         msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
       ),
-      confirmBtnText: 'Block this List',
+      confirmBtnText: _(msg`Block this List`),
       async onPressConfirm() {
         try {
           await listBlockMutation.mutateAsync({uri: list.uri, block: true})
-          Toast.show('List blocked')
+          Toast.show(_(msg`List blocked`))
           track('Lists:Block')
         } catch {
           Toast.show(
-            'There was an issue. Please check your internet connection and try again.',
+            _(
+              msg`There was an issue. Please check your internet connection and try again.`,
+            ),
           )
         }
       },
@@ -330,14 +339,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const onUnsubscribeBlock = useCallback(async () => {
     try {
       await listBlockMutation.mutateAsync({uri: list.uri, block: false})
-      Toast.show('List unblocked')
+      Toast.show(_(msg`List unblocked`))
       track('Lists:Unblock')
     } catch {
       Toast.show(
-        'There was an issue. Please check your internet connection and try again.',
+        _(
+          msg`There was an issue. Please check your internet connection and try again.`,
+        ),
       )
     }
-  }, [list, listBlockMutation, track])
+  }, [list, listBlockMutation, track, _])
 
   const onPressEdit = useCallback(() => {
     openModal({
@@ -353,7 +364,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       message: _(msg`Are you sure?`),
       async onPressConfirm() {
         await listDeleteMutation.mutateAsync({uri: list.uri})
-        Toast.show('List deleted')
+        Toast.show(_(msg`List deleted`))
         track('Lists:Delete')
         if (navigation.canGoBack()) {
           navigation.goBack()
@@ -545,7 +556,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
         <Button
           testID={isPinned ? 'unpinBtn' : 'pinBtn'}
           type={isPinned ? 'default' : 'inverted'}
-          label={isPinned ? 'Unpin' : 'Pin to home'}
+          label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
           onPress={onTogglePinned}
           disabled={isPending}
         />
@@ -554,14 +565,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
           <Button
             testID="unblockBtn"
             type="default"
-            label="Unblock"
+            label={_(msg`Unblock`)}
             onPress={onUnsubscribeBlock}
           />
         ) : isMuting ? (
           <Button
             testID="unmuteBtn"
             type="default"
-            label="Unmute"
+            label={_(msg`Unmute`)}
             onPress={onUnsubscribeMute}
           />
         ) : (
@@ -603,6 +614,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     const [hasNew, setHasNew] = React.useState(false)
     const [isScrolledDown, setIsScrolledDown] = React.useState(false)
     const isScreenFocused = useIsFocused()
+    const {_} = useLingui()
 
     const onScrollToTop = useCallback(() => {
       scrollElRef.current?.scrollToOffset({
@@ -624,8 +636,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     }, [onScrollToTop, isScreenFocused])
 
     const renderPostsEmpty = useCallback(() => {
-      return <EmptyState icon="feed" message="This feed is empty!" />
-    }, [])
+      return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
+    }, [_])
 
     return (
       <View>
@@ -634,6 +646,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
           enabled={isFocused}
           feed={feed}
           pollInterval={60e3}
+          disablePoll={hasNew}
           scrollElRef={scrollElRef}
           onHasNew={setHasNew}
           onScrolledDownChange={setIsScrolledDown}
@@ -643,7 +656,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
         {(isScrolledDown || hasNew) && (
           <LoadLatestBtn
             onPress={onScrollToTop}
-            label="Load new posts"
+            label={_(msg`Load new posts`)}
             showIndicator={hasNew}
           />
         )}
@@ -721,15 +734,30 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
               </Text>
             )}
             <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-              {isCurateList ? 'User list' : 'Moderation list'} by{' '}
-              {isOwner ? (
-                'you'
+              {isCurateList ? (
+                isOwner ? (
+                  <Trans>User list by you</Trans>
+                ) : (
+                  <Trans>
+                    User list by{' '}
+                    <TextLink
+                      text={sanitizeHandle(list.creator.handle || '', '@')}
+                      href={makeProfileLink(list.creator)}
+                      style={pal.textLight}
+                    />
+                  </Trans>
+                )
+              ) : isOwner ? (
+                <Trans>Moderation list by you</Trans>
               ) : (
-                <TextLink
-                  text={sanitizeHandle(list.creator.handle || '', '@')}
-                  href={makeProfileLink(list.creator)}
-                  style={pal.textLight}
-                />
+                <Trans>
+                  Moderation list by{' '}
+                  <TextLink
+                    text={sanitizeHandle(list.creator.handle || '', '@')}
+                    href={makeProfileLink(list.creator)}
+                    style={pal.textLight}
+                  />
+                </Trans>
               )}
             </Text>
           </View>
@@ -782,11 +810,11 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
       return (
         <EmptyState
           icon="users-slash"
-          message="This list is empty!"
+          message={_(msg`This list is empty!`)}
           style={{paddingTop: 40}}
         />
       )
-    }, [])
+    }, [_])
 
     return (
       <View>
@@ -802,7 +830,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
         {isScrolledDown && (
           <LoadLatestBtn
             onPress={onScrollToTop}
-            label="Scroll to top"
+            label={_(msg`Scroll to top`)}
             showIndicator={false}
           />
         )}
@@ -846,7 +874,7 @@ function ErrorScreen({error}: {error: string}) {
         <Button
           type="default"
           accessibilityLabel={_(msg`Go Back`)}
-          accessibilityHint="Return to previous page"
+          accessibilityHint={_(msg`Return to previous page`)}
           onPress={onPressBack}
           style={{flexShrink: 1}}>
           <Text type="button" style={pal.text}>
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index bbac30689..19ae37f0c 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -82,7 +82,7 @@ export function SavedFeeds({}: Props) {
         isTabletOrDesktop && styles.desktopContainer,
       ]}>
       <ViewHeader title={_(msg`Edit My Feeds`)} showOnDesktop showBorder />
-      <ScrollView style={s.flex1}>
+      <ScrollView style={s.flex1} contentContainerStyle={[styles.noBorder]}>
         <View style={[pal.text, pal.border, styles.title]}>
           <Text type="title" style={pal.text}>
             <Trans>Pinned Feeds</Trans>
@@ -160,7 +160,7 @@ export function SavedFeeds({}: Props) {
                 type="sm"
                 style={pal.link}
                 href="https://github.com/bluesky-social/feed-generator"
-                text="See this guide"
+                text={_(msg`See this guide`)}
               />{' '}
               for more information.
             </Trans>
@@ -188,6 +188,7 @@ function ListItem({
   >['reset']
 }) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
   const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
     useUnpinFeedMutation()
@@ -205,10 +206,10 @@ function ListItem({
         await pinFeed({uri: feedUri})
       }
     } catch (e) {
-      Toast.show('There was an issue contacting the server')
+      Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {error: e})
     }
-  }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState])
+  }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _])
 
   const onPressUp = React.useCallback(async () => {
     if (!isPinned) return
@@ -227,10 +228,10 @@ function ListItem({
         index: pinned.indexOf(feedUri),
       })
     } catch (e) {
-      Toast.show('There was an issue contacting the server')
+      Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to set pinned feed order', {error: e})
     }
-  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
 
   const onPressDown = React.useCallback(async () => {
     if (!isPinned) return
@@ -248,10 +249,10 @@ function ListItem({
         index: pinned.indexOf(feedUri),
       })
     } catch (e) {
-      Toast.show('There was an issue contacting the server')
+      Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to set pinned feed order', {error: e})
     }
-  }, [feedUri, isPinned, setSavedFeeds, currentFeeds])
+  }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
 
   return (
     <Pressable
@@ -288,7 +289,7 @@ function ListItem({
       <FeedSourceCard
         key={feedUri}
         feedUri={feedUri}
-        style={styles.noBorder}
+        style={styles.noTopBorder}
         showSaveBtn
         showMinimalPlaceholder
       />
@@ -344,7 +345,7 @@ const styles = StyleSheet.create({
   webArrowUpButton: {
     marginBottom: 10,
   },
-  noBorder: {
+  noTopBorder: {
     borderTopWidth: 0,
   },
   footerText: {
@@ -352,4 +353,10 @@ const styles = StyleSheet.create({
     paddingTop: 22,
     paddingBottom: 100,
   },
+  noBorder: {
+    borderBottomWidth: 0,
+    borderRightWidth: 0,
+    borderLeftWidth: 0,
+    borderTopWidth: 0,
+  },
 })
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index b522edfba..df64cc5aa 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -42,11 +42,17 @@ import {useSetDrawerOpen} from '#/state/shell'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {MagnifyingGlassIcon} from '#/lib/icons'
 import {useModerationOpts} from '#/state/queries/preferences'
-import {SearchResultCard} from '#/view/shell/desktop/Search'
+import {
+  MATCH_HANDLE,
+  SearchLinkCard,
+  SearchProfileCard,
+} from '#/view/shell/desktop/Search'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-import {isWeb} from '#/platform/detection'
+import {isNative, isWeb} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
 import {s} from '#/lib/styles'
+import AsyncStorage from '@react-native-async-storage/async-storage'
+import {augmentSearchQuery} from '#/lib/strings/helpers'
 
 function Loader() {
   const pal = usePalette('default')
@@ -83,9 +89,7 @@ function EmptyState({message, error}: {message: string; error?: string}) {
         },
       ]}>
       <View style={[pal.viewLight, {padding: 18, borderRadius: 8}]}>
-        <Text style={[pal.text]}>
-          <Trans>{message}</Trans>
-        </Text>
+        <Text style={[pal.text]}>{message}</Text>
 
         {error && (
           <>
@@ -162,6 +166,8 @@ function SearchScreenSuggestedFollows() {
       // @ts-ignore web only -prf
       desktopFixedHeight
       contentContainerStyle={{paddingBottom: 1200}}
+      keyboardShouldPersistTaps="handled"
+      keyboardDismissMode="on-drag"
     />
   ) : (
     <CenteredView sideBorders style={[pal.border, s.hContentRegion]}>
@@ -303,13 +309,23 @@ function SearchScreenUserResults({query}: {query: string}) {
 
 const SECTIONS_LOGGEDOUT = ['Users']
 const SECTIONS_LOGGEDIN = ['Posts', 'Users']
-export function SearchScreenInner({query}: {query?: string}) {
+export function SearchScreenInner({
+  query,
+  primarySearch,
+}: {
+  query?: string
+  primarySearch?: boolean
+}) {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-  const {hasSession} = useSession()
+  const {hasSession, currentAccount} = useSession()
   const {isDesktop} = useWebMediaQueries()
 
+  const augmentedQuery = React.useMemo(() => {
+    return augmentSearchQuery(query || '', {did: currentAccount?.did})
+  }, [query, currentAccount])
+
   const onPageSelected = React.useCallback(
     (index: number) => {
       setMinimalShellMode(false)
@@ -324,13 +340,15 @@ export function SearchScreenInner({query}: {query?: string}) {
         tabBarPosition="top"
         onPageSelected={onPageSelected}
         renderTabBar={props => (
-          <CenteredView sideBorders style={pal.border}>
+          <CenteredView
+            sideBorders
+            style={[pal.border, pal.view, styles.tabBarContainer]}>
             <TabBar items={SECTIONS_LOGGEDIN} {...props} />
           </CenteredView>
         )}
         initialPage={0}>
         <View>
-          <SearchScreenPostResults query={query} />
+          <SearchScreenPostResults query={augmentedQuery} />
         </View>
         <View>
           <SearchScreenUserResults query={query} />
@@ -365,7 +383,9 @@ export function SearchScreenInner({query}: {query?: string}) {
       tabBarPosition="top"
       onPageSelected={onPageSelected}
       renderTabBar={props => (
-        <CenteredView sideBorders style={pal.border}>
+        <CenteredView
+          sideBorders
+          style={[pal.border, pal.view, styles.tabBarContainer]}>
           <TabBar items={SECTIONS_LOGGEDOUT} {...props} />
         </CenteredView>
       )}
@@ -413,7 +433,7 @@ export function SearchScreenInner({query}: {query?: string}) {
             style={pal.textLight}
           />
           <Text type="xl" style={[pal.textLight, {paddingHorizontal: 18}]}>
-            {isDesktop ? (
+            {isDesktop && !primarySearch ? (
               <Trans>Find users with the search tool on the right</Trans>
             ) : (
               <Trans>Find users on Bluesky</Trans>
@@ -425,19 +445,7 @@ export function SearchScreenInner({query}: {query?: string}) {
   )
 }
 
-export function SearchScreenDesktop(
-  props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
-) {
-  const {isDesktop} = useWebMediaQueries()
-
-  return isDesktop ? (
-    <SearchScreenInner query={props.route.params?.q} />
-  ) : (
-    <SearchScreenMobile {...props} />
-  )
-}
-
-export function SearchScreenMobile(
+export function SearchScreen(
   props: NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>,
 ) {
   const theme = useTheme()
@@ -449,7 +457,7 @@ export function SearchScreenMobile(
   const moderationOpts = useModerationOpts()
   const search = useActorAutocompleteFn()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const {isTablet} = useWebMediaQueries()
+  const {isTabletOrDesktop, isTabletOrMobile} = useWebMediaQueries()
 
   const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>(
     undefined,
@@ -462,32 +470,56 @@ export function SearchScreenMobile(
   const [inputIsFocused, setInputIsFocused] = React.useState(false)
   const [showAutocompleteResults, setShowAutocompleteResults] =
     React.useState(false)
+  const [searchHistory, setSearchHistory] = React.useState<string[]>([])
+
+  React.useEffect(() => {
+    const loadSearchHistory = async () => {
+      try {
+        const history = await AsyncStorage.getItem('searchHistory')
+        if (history !== null) {
+          setSearchHistory(JSON.parse(history))
+        }
+      } catch (e: any) {
+        logger.error('Failed to load search history', e)
+      }
+    }
+
+    loadSearchHistory()
+  }, [])
 
   const onPressMenu = React.useCallback(() => {
     track('ViewHeader:MenuButtonClicked')
     setDrawerOpen(true)
   }, [track, setDrawerOpen])
+
   const onPressCancelSearch = React.useCallback(() => {
+    scrollToTopWeb()
     textInput.current?.blur()
     setQuery('')
     setShowAutocompleteResults(false)
     if (searchDebounceTimeout.current)
       clearTimeout(searchDebounceTimeout.current)
   }, [textInput])
+
   const onPressClearQuery = React.useCallback(() => {
+    scrollToTopWeb()
     setQuery('')
     setShowAutocompleteResults(false)
   }, [setQuery])
+
   const onChangeText = React.useCallback(
     async (text: string) => {
+      scrollToTopWeb()
+
       setQuery(text)
 
       if (text.length > 0) {
         setIsFetching(true)
         setShowAutocompleteResults(true)
 
-        if (searchDebounceTimeout.current)
+        if (searchDebounceTimeout.current) {
           clearTimeout(searchDebounceTimeout.current)
+        }
 
         searchDebounceTimeout.current = setTimeout(async () => {
           const results = await search({query: text, limit: 30})
@@ -498,8 +530,9 @@ export function SearchScreenMobile(
           }
         }, 300)
       } else {
-        if (searchDebounceTimeout.current)
+        if (searchDebounceTimeout.current) {
           clearTimeout(searchDebounceTimeout.current)
+        }
         setSearchResults([])
         setIsFetching(false)
         setShowAutocompleteResults(false)
@@ -507,14 +540,47 @@ export function SearchScreenMobile(
     },
     [setQuery, search, setSearchResults],
   )
+
+  const updateSearchHistory = React.useCallback(
+    async (newQuery: string) => {
+      newQuery = newQuery.trim()
+      if (newQuery && !searchHistory.includes(newQuery)) {
+        let newHistory = [newQuery, ...searchHistory]
+
+        if (newHistory.length > 5) {
+          newHistory = newHistory.slice(0, 5)
+        }
+
+        setSearchHistory(newHistory)
+        try {
+          await AsyncStorage.setItem(
+            'searchHistory',
+            JSON.stringify(newHistory),
+          )
+        } catch (e: any) {
+          logger.error('Failed to save search history', e)
+        }
+      }
+    },
+    [searchHistory, setSearchHistory],
+  )
+
   const onSubmit = React.useCallback(() => {
+    scrollToTopWeb()
     setShowAutocompleteResults(false)
-  }, [setShowAutocompleteResults])
+    updateSearchHistory(query)
+  }, [query, setShowAutocompleteResults, updateSearchHistory])
 
   const onSoftReset = React.useCallback(() => {
+    scrollToTopWeb()
     onPressCancelSearch()
   }, [onPressCancelSearch])
 
+  const queryMaybeHandle = React.useMemo(() => {
+    const match = MATCH_HANDLE.exec(query)
+    return match && match[1]
+  }, [query])
+
   useFocusEffect(
     React.useCallback(() => {
       setMinimalShellMode(false)
@@ -522,19 +588,47 @@ export function SearchScreenMobile(
     }, [onSoftReset, setMinimalShellMode]),
   )
 
+  const handleHistoryItemClick = (item: React.SetStateAction<string>) => {
+    setQuery(item)
+    onSubmit()
+  }
+
+  const handleRemoveHistoryItem = (itemToRemove: string) => {
+    const updatedHistory = searchHistory.filter(item => item !== itemToRemove)
+    setSearchHistory(updatedHistory)
+    AsyncStorage.setItem('searchHistory', JSON.stringify(updatedHistory)).catch(
+      e => {
+        logger.error('Failed to update search history', e)
+      },
+    )
+  }
+
   return (
-    <View style={{flex: 1}}>
-      <CenteredView style={[styles.header, pal.border]} sideBorders={isTablet}>
-        <Pressable
-          testID="viewHeaderBackOrMenuBtn"
-          onPress={onPressMenu}
-          hitSlop={HITSLOP_10}
-          style={styles.headerMenuBtn}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Menu`)}
-          accessibilityHint="Access navigation links and settings">
-          <FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
-        </Pressable>
+    <View style={isWeb ? null : {flex: 1}}>
+      <CenteredView
+        style={[
+          styles.header,
+          pal.border,
+          pal.view,
+          isTabletOrDesktop && {paddingTop: 10},
+        ]}
+        sideBorders={isTabletOrDesktop}>
+        {isTabletOrMobile && (
+          <Pressable
+            testID="viewHeaderBackOrMenuBtn"
+            onPress={onPressMenu}
+            hitSlop={HITSLOP_10}
+            style={styles.headerMenuBtn}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Menu`)}
+            accessibilityHint={_(msg`Access navigation links and settings`)}>
+            <FontAwesomeIcon
+              icon="bars"
+              size={18}
+              color={pal.colors.textLight}
+            />
+          </Pressable>
+        )}
 
         <View
           style={[
@@ -548,7 +642,7 @@ export function SearchScreenMobile(
           <TextInput
             testID="searchTextInput"
             ref={textInput}
-            placeholder="Search"
+            placeholder={_(msg`Search`)}
             placeholderTextColor={pal.colors.textLight}
             selectTextOnFocus
             returnKeyType="search"
@@ -556,7 +650,12 @@ export function SearchScreenMobile(
             style={[pal.text, styles.headerSearchInput]}
             keyboardAppearance={theme.colorScheme}
             onFocus={() => setInputIsFocused(true)}
-            onBlur={() => setInputIsFocused(false)}
+            onBlur={() => {
+              // HACK
+              // give 100ms to not stop click handlers in the search history
+              // -prf
+              setTimeout(() => setInputIsFocused(false), 100)
+            }}
             onChangeText={onChangeText}
             onSubmitEditing={onSubmit}
             autoFocus={false}
@@ -564,6 +663,7 @@ export function SearchScreenMobile(
             accessibilityLabel={_(msg`Search`)}
             accessibilityHint=""
             autoCorrect={false}
+            autoComplete="off"
             autoCapitalize="none"
           />
           {query ? (
@@ -572,7 +672,8 @@ export function SearchScreenMobile(
               onPress={onPressClearQuery}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Clear search query`)}
-              accessibilityHint="">
+              accessibilityHint=""
+              hitSlop={HITSLOP_10}>
               <FontAwesomeIcon
                 icon="xmark"
                 size={16}
@@ -584,7 +685,10 @@ export function SearchScreenMobile(
 
         {query || inputIsFocused ? (
           <View style={styles.headerCancelBtn}>
-            <Pressable onPress={onPressCancelSearch} accessibilityRole="button">
+            <Pressable
+              onPress={onPressCancelSearch}
+              accessibilityRole="button"
+              hitSlop={HITSLOP_10}>
               <Text style={[pal.text]}>
                 <Trans>Cancel</Trans>
               </Text>
@@ -593,29 +697,83 @@ export function SearchScreenMobile(
         ) : undefined}
       </CenteredView>
 
-      {showAutocompleteResults && moderationOpts ? (
+      {showAutocompleteResults ? (
         <>
-          {isFetching ? (
+          {isFetching || !moderationOpts ? (
             <Loader />
           ) : (
-            <ScrollView style={{height: '100%'}}>
-              {searchResults.length ? (
-                searchResults.map((item, i) => (
-                  <SearchResultCard
-                    key={item.did}
-                    profile={item}
-                    moderation={moderateProfile(item, moderationOpts)}
-                    style={i === 0 ? {borderTopWidth: 0} : {}}
-                  />
-                ))
-              ) : (
-                <EmptyState message={_(msg`No results found for ${query}`)} />
-              )}
+            <ScrollView
+              style={{height: '100%'}}
+              // @ts-ignore web only -prf
+              dataSet={{stableGutters: '1'}}
+              keyboardShouldPersistTaps="handled"
+              keyboardDismissMode="on-drag">
+              <SearchLinkCard
+                label={_(msg`Search for "${query}"`)}
+                onPress={isNative ? onSubmit : undefined}
+                to={
+                  isNative
+                    ? undefined
+                    : `/search?q=${encodeURIComponent(query)}`
+                }
+                style={{borderBottomWidth: 1}}
+              />
+
+              {queryMaybeHandle ? (
+                <SearchLinkCard
+                  label={_(msg`Go to @${queryMaybeHandle}`)}
+                  to={`/profile/${queryMaybeHandle}`}
+                />
+              ) : null}
+
+              {searchResults.map(item => (
+                <SearchProfileCard
+                  key={item.did}
+                  profile={item}
+                  moderation={moderateProfile(item, moderationOpts)}
+                />
+              ))}
 
               <View style={{height: 200}} />
             </ScrollView>
           )}
         </>
+      ) : !query && inputIsFocused ? (
+        <CenteredView
+          sideBorders={isTabletOrDesktop}
+          // @ts-ignore web only -prf
+          style={{
+            height: isWeb ? '100vh' : undefined,
+          }}>
+          <View style={styles.searchHistoryContainer}>
+            {searchHistory.length > 0 && (
+              <View style={styles.searchHistoryContent}>
+                <Text style={[pal.text, styles.searchHistoryTitle]}>
+                  Recent Searches
+                </Text>
+                {searchHistory.map((historyItem, index) => (
+                  <View key={index} style={styles.historyItemContainer}>
+                    <Pressable
+                      accessibilityRole="button"
+                      onPress={() => handleHistoryItemClick(historyItem)}
+                      style={styles.historyItem}>
+                      <Text style={pal.text}>{historyItem}</Text>
+                    </Pressable>
+                    <Pressable
+                      accessibilityRole="button"
+                      onPress={() => handleRemoveHistoryItem(historyItem)}>
+                      <FontAwesomeIcon
+                        icon="xmark"
+                        size={16}
+                        style={pal.textLight as FontAwesomeIconStyle}
+                      />
+                    </Pressable>
+                  </View>
+                ))}
+              </View>
+            )}
+          </View>
+        </CenteredView>
       ) : (
         <SearchScreenInner query={query} />
       )}
@@ -623,12 +781,25 @@ export function SearchScreenMobile(
   )
 }
 
+function scrollToTopWeb() {
+  if (isWeb) {
+    window.scrollTo(0, 0)
+  }
+}
+
+const HEADER_HEIGHT = 50
+
 const styles = StyleSheet.create({
   header: {
     flexDirection: 'row',
     alignItems: 'center',
     paddingHorizontal: 12,
     paddingVertical: 4,
+    height: HEADER_HEIGHT,
+    // @ts-ignore web only
+    position: isWeb ? 'sticky' : '',
+    top: 0,
+    zIndex: 1,
   },
   headerMenuBtn: {
     width: 30,
@@ -658,4 +829,30 @@ const styles = StyleSheet.create({
   headerCancelBtn: {
     paddingLeft: 10,
   },
+  tabBarContainer: {
+    // @ts-ignore web only
+    position: isWeb ? 'sticky' : '',
+    top: isWeb ? HEADER_HEIGHT : 0,
+    zIndex: 1,
+  },
+  searchHistoryContainer: {
+    width: '100%',
+    paddingHorizontal: 12,
+  },
+  searchHistoryContent: {
+    padding: 10,
+    borderRadius: 8,
+  },
+  searchHistoryTitle: {
+    fontWeight: 'bold',
+  },
+  historyItem: {
+    paddingVertical: 8,
+  },
+  historyItemContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 8,
+  },
 })
diff --git a/src/view/screens/Search/index.tsx b/src/view/screens/Search/index.tsx
index a65149bf7..f6c0eca26 100644
--- a/src/view/screens/Search/index.tsx
+++ b/src/view/screens/Search/index.tsx
@@ -1,3 +1 @@
-import {SearchScreenMobile} from '#/view/screens/Search/Search'
-
-export const SearchScreen = SearchScreenMobile
+export {SearchScreen} from '#/view/screens/Search/Search'
diff --git a/src/view/screens/Search/index.web.tsx b/src/view/screens/Search/index.web.tsx
deleted file mode 100644
index 8e039e3cd..000000000
--- a/src/view/screens/Search/index.web.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import {SearchScreenDesktop} from '#/view/screens/Search/Search'
-
-export const SearchScreen = SearchScreenDesktop
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index d48112dae..3b50c5449 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -19,7 +19,6 @@ import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import * as AppInfo from 'lib/app-info'
 import {s, colors} from 'lib/styles'
 import {ScrollView} from '../com/util/Views'
-import {ViewHeader} from '../com/util/ViewHeader'
 import {Link, TextLink} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
 import * as Toast from '../com/util/Toast'
@@ -36,6 +35,7 @@ import {HandIcon, HashtagIcon} from 'lib/icons'
 import Clipboard from '@react-native-clipboard/clipboard'
 import {makeProfileLink} from 'lib/routes/links'
 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
+import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader'
 import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
 import {useModalControls} from '#/state/modals'
 import {
@@ -70,9 +70,15 @@ import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useCloseAllActiveElements} from '#/state/util'
+import {
+  useInAppBrowser,
+  useSetInAppBrowser,
+} from '#/state/preferences/in-app-browser'
+import {isNative} from '#/platform/detection'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {isSwitchingAccounts, currentAccount} = useSession()
   const {logout} = useSessionApi()
   const {data: profile} = useProfileQuery({did: account.did})
@@ -98,10 +104,10 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
           testID="signOutBtn"
           onPress={logout}
           accessibilityRole="button"
-          accessibilityLabel="Sign out"
+          accessibilityLabel={_(msg`Sign out`)}
           accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
           <Text type="lg" style={pal.link}>
-            Sign out
+            <Trans>Sign out</Trans>
           </Text>
         </TouchableOpacity>
       ) : (
@@ -116,7 +122,7 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
         did: currentAccount?.did,
         handle: currentAccount?.handle,
       })}
-      title="Your profile"
+      title={_(msg`Your profile`)}
       noFeedback>
       {contents}
     </Link>
@@ -128,8 +134,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
         isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
       }
       accessibilityRole="button"
-      accessibilityLabel={`Switch to ${account.handle}`}
-      accessibilityHint="Switches the account you are logged in to">
+      accessibilityLabel={_(msg`Switch to ${account.handle}`)}
+      accessibilityHint={_(msg`Switches the account you are logged in to`)}>
       {contents}
     </TouchableOpacity>
   )
@@ -145,6 +151,8 @@ export function SettingsScreen({}: Props) {
   const setMinimalShellMode = useSetMinimalShellMode()
   const requireAltTextEnabled = useRequireAltTextEnabled()
   const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
+  const inAppBrowserPref = useInAppBrowser()
+  const setUseInAppBrowser = useSetInAppBrowser()
   const onboardingDispatch = useOnboardingDispatch()
   const navigation = useNavigation<NavigationProp>()
   const {isMobile} = useWebMediaQueries()
@@ -225,15 +233,15 @@ export function SettingsScreen({}: Props) {
 
   const onPressResetOnboarding = React.useCallback(async () => {
     onboardingDispatch({type: 'start'})
-    Toast.show('Onboarding reset')
-  }, [onboardingDispatch])
+    Toast.show(_(msg`Onboarding reset`))
+  }, [onboardingDispatch, _])
 
   const onPressBuildInfo = React.useCallback(() => {
     Clipboard.setString(
       `Build version: ${AppInfo.appVersion}; Platform: ${Platform.OS}`,
     )
-    Toast.show('Copied build version to clipboard')
-  }, [])
+    Toast.show(_(msg`Copied build version to clipboard`))
+  }, [_])
 
   const openHomeFeedPreferences = React.useCallback(() => {
     navigation.navigate('PreferencesHomeFeed')
@@ -265,20 +273,34 @@ export function SettingsScreen({}: Props) {
 
   const clearAllStorage = React.useCallback(async () => {
     await clearStorage()
-    Toast.show(`Storage cleared, you need to restart the app now.`)
-  }, [])
+    Toast.show(_(msg`Storage cleared, you need to restart the app now.`))
+  }, [_])
   const clearAllLegacyStorage = React.useCallback(async () => {
     await clearLegacyStorage()
-    Toast.show(`Legacy storage cleared, you need to restart the app now.`)
-  }, [])
+    Toast.show(_(msg`Legacy storage cleared, you need to restart the app now.`))
+  }, [_])
 
   return (
-    <View style={[s.hContentRegion]} testID="settingsScreen">
-      <ViewHeader title={_(msg`Settings`)} />
+    <View style={s.hContentRegion} testID="settingsScreen">
+      <SimpleViewHeader
+        showBackButton={isMobile}
+        style={[
+          pal.border,
+          {borderBottomWidth: 1},
+          !isMobile && {borderLeftWidth: 1, borderRightWidth: 1},
+        ]}>
+        <View style={{flex: 1}}>
+          <Text type="title-lg" style={[pal.text, {fontWeight: 'bold'}]}>
+            <Trans>Settings</Trans>
+          </Text>
+        </View>
+      </SimpleViewHeader>
       <ScrollView
         style={[s.hContentRegion]}
         contentContainerStyle={isMobile && pal.viewLight}
-        scrollIndicatorInsets={{right: 1}}>
+        scrollIndicatorInsets={{right: 1}}
+        // @ts-ignore web only -prf
+        dataSet={{'stable-gutters': 1}}>
         <View style={styles.spacer20} />
         {currentAccount ? (
           <>
@@ -298,12 +320,18 @@ export function SettingsScreen({}: Props) {
                   />
                 </>
               )}
-              <Text type="lg" style={pal.text}>
-                {currentAccount.email || '(no email)'}{' '}
+              <Text
+                type="lg"
+                numberOfLines={1}
+                style={[
+                  pal.text,
+                  {overflow: 'hidden', marginRight: 4, flex: 1},
+                ]}>
+                {currentAccount.email || '(no email)'}
               </Text>
               <Link onPress={() => openModal({name: 'change-email'})}>
                 <Text type="lg" style={pal.link}>
-                  <Trans>Change</Trans>
+                  <Trans context="action">Change</Trans>
                 </Text>
               </Link>
             </View>
@@ -353,7 +381,7 @@ export function SettingsScreen({}: Props) {
           onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Add account`)}
-          accessibilityHint="Create a new Bluesky account">
+          accessibilityHint={_(msg`Create a new Bluesky account`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="plus"
@@ -381,7 +409,7 @@ export function SettingsScreen({}: Props) {
           onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Invite`)}
-          accessibilityHint="Opens invite code list"
+          accessibilityHint={_(msg`Opens invite code list`)}
           disabled={invites?.disabled}>
           <View
             style={[
@@ -419,7 +447,7 @@ export function SettingsScreen({}: Props) {
         <View style={[pal.view, styles.toggleCard]}>
           <ToggleButton
             type="default-light"
-            label="Require alt text before posting"
+            label={_(msg`Require alt text before posting`)}
             labelType="lg"
             isSelected={requireAltTextEnabled}
             onPress={() => setRequireAltTextEnabled(!requireAltTextEnabled)}
@@ -435,23 +463,23 @@ export function SettingsScreen({}: Props) {
           <View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
             <SelectableBtn
               selected={colorMode === 'system'}
-              label="System"
+              label={_(msg`System`)}
               left
               onSelect={() => setColorMode('system')}
-              accessibilityHint="Set color theme to system setting"
+              accessibilityHint={_(msg`Set color theme to system setting`)}
             />
             <SelectableBtn
               selected={colorMode === 'light'}
-              label="Light"
+              label={_(msg`Light`)}
               onSelect={() => setColorMode('light')}
-              accessibilityHint="Set color theme to light"
+              accessibilityHint={_(msg`Set color theme to light`)}
             />
             <SelectableBtn
               selected={colorMode === 'dark'}
-              label="Dark"
+              label={_(msg`Dark`)}
               right
               onSelect={() => setColorMode('dark')}
-              accessibilityHint="Set color theme to dark"
+              accessibilityHint={_(msg`Set color theme to dark`)}
             />
           </View>
         </View>
@@ -529,8 +557,8 @@ export function SettingsScreen({}: Props) {
           ]}
           onPress={isSwitchingAccounts ? undefined : onPressLanguageSettings}
           accessibilityRole="button"
-          accessibilityHint="Language settings"
-          accessibilityLabel={_(msg`Opens configurable language settings`)}>
+          accessibilityLabel={_(msg`Language settings`)}
+          accessibilityHint={_(msg`Opens configurable language settings`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="language"
@@ -554,8 +582,8 @@ export function SettingsScreen({}: Props) {
               : () => navigation.navigate('Moderation')
           }
           accessibilityRole="button"
-          accessibilityHint=""
-          accessibilityLabel={_(msg`Opens moderation settings`)}>
+          accessibilityLabel={_(msg`Moderation settings`)}
+          accessibilityHint={_(msg`Opens moderation settings`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <HandIcon style={pal.text} size={18} strokeWidth={6} />
           </View>
@@ -563,6 +591,39 @@ export function SettingsScreen({}: Props) {
             <Trans>Moderation</Trans>
           </Text>
         </TouchableOpacity>
+
+        <View style={styles.spacer20} />
+
+        <Text type="xl-bold" style={[pal.text, styles.heading]}>
+          <Trans>Privacy</Trans>
+        </Text>
+
+        <TouchableOpacity
+          testID="externalEmbedsBtn"
+          style={[
+            styles.linkCard,
+            pal.view,
+            isSwitchingAccounts && styles.dimmed,
+          ]}
+          onPress={
+            isSwitchingAccounts
+              ? undefined
+              : () => navigation.navigate('PreferencesExternalEmbeds')
+          }
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`External media settings`)}
+          accessibilityHint={_(msg`Opens external embeds settings`)}>
+          <View style={[styles.iconContainer, pal.btn]}>
+            <FontAwesomeIcon
+              icon={['far', 'circle-play']}
+              style={pal.text as FontAwesomeIconStyle}
+            />
+          </View>
+          <Text type="lg" style={pal.text}>
+            <Trans>External Media Preferences</Trans>
+          </Text>
+        </TouchableOpacity>
+
         <View style={styles.spacer20} />
 
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
@@ -577,8 +638,8 @@ export function SettingsScreen({}: Props) {
           ]}
           onPress={onPressAppPasswords}
           accessibilityRole="button"
-          accessibilityHint="Open app password settings"
-          accessibilityLabel={_(msg`Opens the app password settings page`)}>
+          accessibilityLabel={_(msg`App password settings`)}
+          accessibilityHint={_(msg`Opens the app password settings page`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="lock"
@@ -599,7 +660,7 @@ export function SettingsScreen({}: Props) {
           onPress={isSwitchingAccounts ? undefined : onPressChangeHandle}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Change handle`)}
-          accessibilityHint="Choose a new Bluesky username or create">
+          accessibilityHint={_(msg`Choose a new Bluesky username or create`)}>
           <View style={[styles.iconContainer, pal.btn]}>
             <FontAwesomeIcon
               icon="at"
@@ -610,6 +671,17 @@ export function SettingsScreen({}: Props) {
             <Trans>Change handle</Trans>
           </Text>
         </TouchableOpacity>
+        {isNative && (
+          <View style={[pal.view, styles.toggleCard]}>
+            <ToggleButton
+              type="default-light"
+              label={_(msg`Open links with in-app browser`)}
+              labelType="lg"
+              isSelected={inAppBrowserPref ?? false}
+              onPress={() => setUseInAppBrowser(!inAppBrowserPref)}
+            />
+          </View>
+        )}
         <View style={styles.spacer20} />
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
           <Trans>Danger Zone</Trans>
@@ -620,7 +692,9 @@ export function SettingsScreen({}: Props) {
           accessible={true}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Delete account`)}
-          accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
+          accessibilityHint={_(
+            msg`Opens modal for account deletion confirmation. Requires email code.`,
+          )}>
           <View style={[styles.iconContainer, dangerBg]}>
             <FontAwesomeIcon
               icon={['far', 'trash-can']}
@@ -660,8 +734,8 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressStorybook}
               accessibilityRole="button"
-              accessibilityHint="Open storybook page"
-              accessibilityLabel={_(msg`Opens the storybook page`)}>
+              accessibilityLabel={_(msg`Open storybook page`)}
+              accessibilityHint={_(msg`Opens the storybook page`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Storybook</Trans>
               </Text>
@@ -670,8 +744,8 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressResetPreferences}
               accessibilityRole="button"
-              accessibilityHint="Reset preferences"
-              accessibilityLabel={_(msg`Resets the preferences state`)}>
+              accessibilityLabel={_(msg`Reset preferences`)}
+              accessibilityHint={_(msg`Resets the preferences state`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Reset preferences state</Trans>
               </Text>
@@ -680,8 +754,8 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={onPressResetOnboarding}
               accessibilityRole="button"
-              accessibilityHint="Reset onboarding"
-              accessibilityLabel={_(msg`Resets the onboarding state`)}>
+              accessibilityLabel={_(msg`Reset onboarding`)}
+              accessibilityHint={_(msg`Resets the onboarding state`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Reset onboarding state</Trans>
               </Text>
@@ -690,8 +764,8 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={clearAllLegacyStorage}
               accessibilityRole="button"
-              accessibilityHint="Clear all legacy storage data"
-              accessibilityLabel={_(msg`Clear all legacy storage data`)}>
+              accessibilityLabel={_(msg`Clear all legacy storage data`)}
+              accessibilityHint={_(msg`Clear all legacy storage data`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>
                   Clear all legacy storage data (restart after this)
@@ -702,8 +776,8 @@ export function SettingsScreen({}: Props) {
               style={[pal.view, styles.linkCardNoIcon]}
               onPress={clearAllStorage}
               accessibilityRole="button"
-              accessibilityHint="Clear all storage data"
-              accessibilityLabel={_(msg`Clear all storage data`)}>
+              accessibilityLabel={_(msg`Clear all storage data`)}
+              accessibilityHint={_(msg`Clear all storage data`)}>
               <Text type="lg" style={pal.text}>
                 <Trans>Clear all storage data (restart after this)</Trans>
               </Text>
diff --git a/src/view/screens/Storybook/Breakpoints.tsx b/src/view/screens/Storybook/Breakpoints.tsx
new file mode 100644
index 000000000..1b846d517
--- /dev/null
+++ b/src/view/screens/Storybook/Breakpoints.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme, useBreakpoints} from '#/alf'
+import {Text, H3} from '#/components/Typography'
+
+export function Breakpoints() {
+  const t = useTheme()
+  const breakpoints = useBreakpoints()
+
+  return (
+    <View>
+      <H3 style={[a.pb_md]}>Breakpoint Debugger</H3>
+      <Text style={[a.pb_md]}>
+        Current breakpoint: {!breakpoints.gtMobile && <Text>mobile</Text>}
+        {breakpoints.gtMobile && !breakpoints.gtTablet && <Text>tablet</Text>}
+        {breakpoints.gtTablet && <Text>desktop</Text>}
+      </Text>
+      <Text
+        style={[a.p_md, t.atoms.bg_contrast_100, {fontFamily: 'monospace'}]}>
+        {JSON.stringify(breakpoints, null, 2)}
+      </Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Buttons.tsx b/src/view/screens/Storybook/Buttons.tsx
new file mode 100644
index 000000000..fbdc84eb4
--- /dev/null
+++ b/src/view/screens/Storybook/Buttons.tsx
@@ -0,0 +1,124 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {
+  Button,
+  ButtonVariant,
+  ButtonColor,
+  ButtonIcon,
+  ButtonText,
+} from '#/components/Button'
+import {H1} from '#/components/Typography'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+
+export function Buttons() {
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Buttons</H1>
+
+      <View style={[a.flex_row, a.flex_wrap, a.gap_md, a.align_start]}>
+        {['primary', 'secondary', 'negative'].map(color => (
+          <View key={color} style={[a.gap_md, a.align_start]}>
+            {['solid', 'outline', 'ghost'].map(variant => (
+              <React.Fragment key={variant}>
+                <Button
+                  variant={variant as ButtonVariant}
+                  color={color as ButtonColor}
+                  size="large"
+                  label="Click here">
+                  Button
+                </Button>
+                <Button
+                  disabled
+                  variant={variant as ButtonVariant}
+                  color={color as ButtonColor}
+                  size="large"
+                  label="Click here">
+                  Button
+                </Button>
+              </React.Fragment>
+            ))}
+          </View>
+        ))}
+
+        <View style={[a.flex_row, a.gap_md, a.align_start]}>
+          <View style={[a.gap_md, a.align_start]}>
+            {['gradient_sky', 'gradient_midnight', 'gradient_sunrise'].map(
+              name => (
+                <React.Fragment key={name}>
+                  <Button
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                  <Button
+                    disabled
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                </React.Fragment>
+              ),
+            )}
+          </View>
+          <View style={[a.gap_md, a.align_start]}>
+            {['gradient_sunset', 'gradient_nordic', 'gradient_bonfire'].map(
+              name => (
+                <React.Fragment key={name}>
+                  <Button
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                  <Button
+                    disabled
+                    variant="gradient"
+                    color={name as ButtonColor}
+                    size="large"
+                    label="Click here">
+                    Button
+                  </Button>
+                </React.Fragment>
+              ),
+            )}
+          </View>
+        </View>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="large"
+          label="Link out">
+          <ButtonText>Link out</ButtonText>
+          <ButtonIcon icon={ArrowTopRight} />
+        </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="small"
+          label="Link out">
+          <ButtonText>Link out</ButtonText>
+          <ButtonIcon icon={ArrowTopRight} />
+        </Button>
+
+        <Button
+          variant="gradient"
+          color="gradient_sky"
+          size="small"
+          label="Link out">
+          <ButtonIcon icon={Globe} />
+          <ButtonText>See the world</ButtonText>
+        </Button>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx
new file mode 100644
index 000000000..db568c6bd
--- /dev/null
+++ b/src/view/screens/Storybook/Dialogs.tsx
@@ -0,0 +1,90 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {Button} from '#/components/Button'
+import {H3, P} from '#/components/Typography'
+import * as Dialog from '#/components/Dialog'
+import * as Prompt from '#/components/Prompt'
+import {useDialogStateControlContext} from '#/state/dialogs'
+
+export function Dialogs() {
+  const control = Dialog.useDialogControl()
+  const prompt = Prompt.usePromptControl()
+  const {closeAllDialogs} = useDialogStateControlContext()
+
+  return (
+    <View style={[a.gap_md]}>
+      <Button
+        variant="outline"
+        color="secondary"
+        size="small"
+        onPress={() => {
+          control.open()
+          prompt.open()
+        }}
+        label="Open basic dialog">
+        Open basic dialog
+      </Button>
+
+      <Button
+        variant="solid"
+        color="primary"
+        size="small"
+        onPress={() => prompt.open()}
+        label="Open prompt">
+        Open prompt
+      </Button>
+
+      <Prompt.Outer control={prompt}>
+        <Prompt.Title>This is a prompt</Prompt.Title>
+        <Prompt.Description>
+          This is a generic prompt component. It accepts a title and a
+          description, as well as two actions.
+        </Prompt.Description>
+        <Prompt.Actions>
+          <Prompt.Cancel>Cancel</Prompt.Cancel>
+          <Prompt.Action>Confirm</Prompt.Action>
+        </Prompt.Actions>
+      </Prompt.Outer>
+
+      <Dialog.Outer
+        control={control}
+        nativeOptions={{sheet: {snapPoints: ['90%']}}}>
+        <Dialog.Handle />
+
+        <Dialog.ScrollableInner
+          accessibilityDescribedBy="dialog-description"
+          accessibilityLabelledBy="dialog-title">
+          <View style={[a.relative, a.gap_md, a.w_full]}>
+            <H3 nativeID="dialog-title">Dialog</H3>
+            <P nativeID="dialog-description">
+              A scrollable dialog with an input within it.
+            </P>
+            <Dialog.Input value="" onChangeText={() => {}} label="Type here" />
+
+            <Button
+              variant="outline"
+              color="secondary"
+              size="small"
+              onPress={closeAllDialogs}
+              label="Close all dialogs">
+              Close all dialogs
+            </Button>
+            <View style={{height: 1000}} />
+            <View style={[a.flex_row, a.justify_end]}>
+              <Button
+                variant="outline"
+                color="primary"
+                size="small"
+                onPress={() => control.close()}
+                label="Open basic dialog">
+                Close basic dialog
+              </Button>
+            </View>
+          </View>
+        </Dialog.ScrollableInner>
+      </Dialog.Outer>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx
new file mode 100644
index 000000000..9396cca67
--- /dev/null
+++ b/src/view/screens/Storybook/Forms.tsx
@@ -0,0 +1,215 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {H1, H3} from '#/components/Typography'
+import * as TextField from '#/components/forms/TextField'
+import {DateField, Label} from '#/components/forms/DateField'
+import * as Toggle from '#/components/forms/Toggle'
+import * as ToggleButton from '#/components/forms/ToggleButton'
+import {Button} from '#/components/Button'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+
+export function Forms() {
+  const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a'])
+  const [toggleGroupBValues, setToggleGroupBValues] = React.useState(['a', 'b'])
+  const [toggleGroupCValues, setToggleGroupCValues] = React.useState(['a', 'b'])
+  const [toggleGroupDValues, setToggleGroupDValues] = React.useState(['warn'])
+
+  const [value, setValue] = React.useState('')
+  const [date, setDate] = React.useState('2001-01-01')
+
+  return (
+    <View style={[a.gap_4xl, a.align_start]}>
+      <H1>Forms</H1>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>InputText</H3>
+
+        <TextField.Input
+          value={value}
+          onChangeText={setValue}
+          label="Text field"
+        />
+
+        <TextField.Root>
+          <TextField.Icon icon={Globe} />
+          <TextField.Input
+            value={value}
+            onChangeText={setValue}
+            label="Text field"
+          />
+        </TextField.Root>
+
+        <View style={[a.w_full]}>
+          <TextField.Label>Text field</TextField.Label>
+          <TextField.Root>
+            <TextField.Icon icon={Globe} />
+            <TextField.Input
+              value={value}
+              onChangeText={setValue}
+              label="Text field"
+            />
+            <TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix>
+          </TextField.Root>
+        </View>
+
+        <View style={[a.w_full]}>
+          <TextField.Label>Textarea</TextField.Label>
+          <TextField.Input
+            multiline
+            numberOfLines={4}
+            value={value}
+            onChangeText={setValue}
+            label="Text field"
+          />
+        </View>
+
+        <H3>DateField</H3>
+
+        <View style={[a.w_full]}>
+          <Label>Date</Label>
+          <DateField
+            testID="date"
+            value={date}
+            onChangeDate={date => {
+              console.log(date)
+              setDate(date)
+            }}
+            label="Input"
+          />
+        </View>
+      </View>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>Toggles</H3>
+
+        <Toggle.Item name="a" label="Click me">
+          <Toggle.Checkbox />
+          <Toggle.Label>Uncontrolled toggle</Toggle.Label>
+        </Toggle.Item>
+
+        <Toggle.Group
+          label="Toggle"
+          type="checkbox"
+          maxSelections={2}
+          values={toggleGroupAValues}
+          onChange={setToggleGroupAValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Switch />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+
+        <Toggle.Group
+          label="Toggle"
+          type="checkbox"
+          maxSelections={2}
+          values={toggleGroupBValues}
+          onChange={setToggleGroupBValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Checkbox />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+
+        <Toggle.Group
+          label="Toggle"
+          type="radio"
+          values={toggleGroupCValues}
+          onChange={setToggleGroupCValues}>
+          <View style={[a.gap_md]}>
+            <Toggle.Item name="a" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="b" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="c" label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="d" disabled label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+            <Toggle.Item name="e" isInvalid label="Click me">
+              <Toggle.Radio />
+              <Toggle.Label>Click me</Toggle.Label>
+            </Toggle.Item>
+          </View>
+        </Toggle.Group>
+      </View>
+
+      <Button
+        variant="gradient"
+        color="gradient_nordic"
+        size="small"
+        label="Reset all toggles"
+        onPress={() => {
+          setToggleGroupAValues(['a'])
+          setToggleGroupBValues(['a', 'b'])
+          setToggleGroupCValues(['a'])
+        }}>
+        Reset all toggles
+      </Button>
+
+      <View style={[a.gap_md, a.align_start, a.w_full]}>
+        <H3>ToggleButton</H3>
+
+        <ToggleButton.Group
+          label="Preferences"
+          values={toggleGroupDValues}
+          onChange={setToggleGroupDValues}>
+          <ToggleButton.Button name="hide" label="Hide">
+            Hide
+          </ToggleButton.Button>
+          <ToggleButton.Button name="warn" label="Warn">
+            Warn
+          </ToggleButton.Button>
+          <ToggleButton.Button name="show" label="Show">
+            Show
+          </ToggleButton.Button>
+        </ToggleButton.Group>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Icons.tsx b/src/view/screens/Storybook/Icons.tsx
new file mode 100644
index 000000000..73466e077
--- /dev/null
+++ b/src/view/screens/Storybook/Icons.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {H1} from '#/components/Typography'
+import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
+import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRight} from '#/components/icons/ArrowTopRight'
+import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
+
+export function Icons() {
+  const t = useTheme()
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Icons</H1>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <Globe size="xs" fill={t.atoms.text.color} />
+        <Globe size="sm" fill={t.atoms.text.color} />
+        <Globe size="md" fill={t.atoms.text.color} />
+        <Globe size="lg" fill={t.atoms.text.color} />
+        <Globe size="xl" fill={t.atoms.text.color} />
+      </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <ArrowTopRight size="xs" fill={t.atoms.text.color} />
+        <ArrowTopRight size="sm" fill={t.atoms.text.color} />
+        <ArrowTopRight size="md" fill={t.atoms.text.color} />
+        <ArrowTopRight size="lg" fill={t.atoms.text.color} />
+        <ArrowTopRight size="xl" fill={t.atoms.text.color} />
+      </View>
+
+      <View style={[a.flex_row, a.gap_xl]}>
+        <CalendarDays size="xs" fill={t.atoms.text.color} />
+        <CalendarDays size="sm" fill={t.atoms.text.color} />
+        <CalendarDays size="md" fill={t.atoms.text.color} />
+        <CalendarDays size="lg" fill={t.atoms.text.color} />
+        <CalendarDays size="xl" fill={t.atoms.text.color} />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Links.tsx b/src/view/screens/Storybook/Links.tsx
new file mode 100644
index 000000000..c3b1c0e0f
--- /dev/null
+++ b/src/view/screens/Storybook/Links.tsx
@@ -0,0 +1,48 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {ButtonText} from '#/components/Button'
+import {Link} from '#/components/Link'
+import {H1, H3} from '#/components/Typography'
+
+export function Links() {
+  return (
+    <View style={[a.gap_md, a.align_start]}>
+      <H1>Links</H1>
+
+      <View style={[a.gap_md, a.align_start]}>
+        <Link
+          to="https://blueskyweb.xyz"
+          warnOnMismatchingTextChild
+          style={[a.text_md]}>
+          External
+        </Link>
+        <Link to="https://blueskyweb.xyz" style={[a.text_md]}>
+          <H3>External with custom children</H3>
+        </Link>
+        <Link
+          to="https://blueskyweb.xyz"
+          warnOnMismatchingTextChild
+          style={[a.text_lg]}>
+          https://blueskyweb.xyz
+        </Link>
+        <Link
+          to="https://bsky.app/profile/bsky.app"
+          warnOnMismatchingTextChild
+          style={[a.text_md]}>
+          Internal
+        </Link>
+
+        <Link
+          variant="solid"
+          color="primary"
+          size="large"
+          label="View @bsky.app's profile"
+          to="https://bsky.app/profile/bsky.app">
+          <ButtonText>Link as a button</ButtonText>
+        </Link>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Palette.tsx b/src/view/screens/Storybook/Palette.tsx
new file mode 100644
index 000000000..b521fe860
--- /dev/null
+++ b/src/view/screens/Storybook/Palette.tsx
@@ -0,0 +1,336 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import * as tokens from '#/alf/tokens'
+import {atoms as a} from '#/alf'
+
+export function Palette() {
+  return (
+    <View style={[a.gap_md]}>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.gray_0}]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_975},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.gray_1000},
+          ]}
+        />
+      </View>
+
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.blue_975},
+          ]}
+        />
+      </View>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_25},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_50},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.green_975},
+          ]}
+        />
+      </View>
+      <View style={[a.flex_row, a.gap_md]}>
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_25}]}
+        />
+        <View
+          style={[a.flex_1, {height: 60, backgroundColor: tokens.color.red_50}]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_100},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_200},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_300},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_400},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_500},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_600},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_700},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_800},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_900},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_950},
+          ]}
+        />
+        <View
+          style={[
+            a.flex_1,
+            {height: 60, backgroundColor: tokens.color.red_975},
+          ]}
+        />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Shadows.tsx b/src/view/screens/Storybook/Shadows.tsx
new file mode 100644
index 000000000..f92112395
--- /dev/null
+++ b/src/view/screens/Storybook/Shadows.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {H1, Text} from '#/components/Typography'
+
+export function Shadows() {
+  const t = useTheme()
+
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Shadows</H1>
+
+      <View style={[a.flex_row, a.gap_5xl]}>
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_sm,
+          ]}>
+          <Text>shadow_sm</Text>
+        </View>
+
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_md,
+          ]}>
+          <Text>shadow_md</Text>
+        </View>
+
+        <View
+          style={[
+            a.flex_1,
+            a.justify_center,
+            a.px_lg,
+            a.py_2xl,
+            t.atoms.bg,
+            t.atoms.shadow_lg,
+          ]}>
+          <Text>shadow_lg</Text>
+        </View>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Spacing.tsx b/src/view/screens/Storybook/Spacing.tsx
new file mode 100644
index 000000000..d7faf93a8
--- /dev/null
+++ b/src/view/screens/Storybook/Spacing.tsx
@@ -0,0 +1,64 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text, H1} from '#/components/Typography'
+
+export function Spacing() {
+  const t = useTheme()
+  return (
+    <View style={[a.gap_md]}>
+      <H1>Spacing</H1>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>2xs (2px)</Text>
+        <View style={[a.flex_1, a.pt_2xs, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>xs (4px)</Text>
+        <View style={[a.flex_1, a.pt_xs, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>sm (8px)</Text>
+        <View style={[a.flex_1, a.pt_sm, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>md (12px)</Text>
+        <View style={[a.flex_1, a.pt_md, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>lg (16px)</Text>
+        <View style={[a.flex_1, a.pt_lg, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>xl (20px)</Text>
+        <View style={[a.flex_1, a.pt_xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>2xl (24px)</Text>
+        <View style={[a.flex_1, a.pt_2xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>3xl (28px)</Text>
+        <View style={[a.flex_1, a.pt_3xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>4xl (32px)</Text>
+        <View style={[a.flex_1, a.pt_4xl, t.atoms.bg_contrast_300]} />
+      </View>
+
+      <View style={[a.flex_row, a.align_center]}>
+        <Text style={{width: 80}}>5xl (40px)</Text>
+        <View style={[a.flex_1, a.pt_5xl, t.atoms.bg_contrast_300]} />
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Theming.tsx b/src/view/screens/Storybook/Theming.tsx
new file mode 100644
index 000000000..a05443473
--- /dev/null
+++ b/src/view/screens/Storybook/Theming.tsx
@@ -0,0 +1,56 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Text} from '#/components/Typography'
+import {Palette} from './Palette'
+
+export function Theming() {
+  const t = useTheme()
+
+  return (
+    <View style={[t.atoms.bg, a.gap_lg, a.p_xl]}>
+      <Palette />
+
+      <Text style={[a.font_bold, a.pt_xl, a.px_md]}>theme.atoms.text</Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_600, a.px_md]}>
+        theme.atoms.text_contrast_600
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_500, a.px_md]}>
+        theme.atoms.text_contrast_500
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border, a.border_t]} />
+      <Text style={[a.font_bold, t.atoms.text_contrast_400, a.px_md]}>
+        theme.atoms.text_contrast_400
+      </Text>
+
+      <View style={[a.flex_1, t.atoms.border_contrast, a.border_t]} />
+
+      <View style={[a.w_full, a.gap_md]}>
+        <View style={[t.atoms.bg, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_25, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_25</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_50, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_50</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_100, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_100</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_200, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_200</Text>
+        </View>
+        <View style={[t.atoms.bg_contrast_300, a.justify_center, a.p_md]}>
+          <Text>theme.atoms.bg_contrast_300</Text>
+        </View>
+      </View>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/Typography.tsx b/src/view/screens/Storybook/Typography.tsx
new file mode 100644
index 000000000..2e1f04a66
--- /dev/null
+++ b/src/view/screens/Storybook/Typography.tsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {atoms as a} from '#/alf'
+import {Text, H1, H2, H3, H4, H5, H6, P} from '#/components/Typography'
+
+export function Typography() {
+  return (
+    <View style={[a.gap_md]}>
+      <H1>H1 Heading</H1>
+      <H2>H2 Heading</H2>
+      <H3>H3 Heading</H3>
+      <H4>H4 Heading</H4>
+      <H5>H5 Heading</H5>
+      <H6>H6 Heading</H6>
+      <P>P Paragraph</P>
+
+      <Text style={[a.text_5xl]}>atoms.text_5xl</Text>
+      <Text style={[a.text_4xl]}>atoms.text_4xl</Text>
+      <Text style={[a.text_3xl]}>atoms.text_3xl</Text>
+      <Text style={[a.text_2xl]}>atoms.text_2xl</Text>
+      <Text style={[a.text_xl]}>atoms.text_xl</Text>
+      <Text style={[a.text_lg]}>atoms.text_lg</Text>
+      <Text style={[a.text_md]}>atoms.text_md</Text>
+      <Text style={[a.text_sm]}>atoms.text_sm</Text>
+      <Text style={[a.text_xs]}>atoms.text_xs</Text>
+      <Text style={[a.text_2xs]}>atoms.text_2xs</Text>
+    </View>
+  )
+}
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
new file mode 100644
index 000000000..d8898f20e
--- /dev/null
+++ b/src/view/screens/Storybook/index.tsx
@@ -0,0 +1,78 @@
+import React from 'react'
+import {View} from 'react-native'
+import {CenteredView, ScrollView} from '#/view/com/util/Views'
+
+import {atoms as a, useTheme, ThemeProvider} from '#/alf'
+import {useSetColorMode} from '#/state/shell'
+import {Button} from '#/components/Button'
+
+import {Theming} from './Theming'
+import {Typography} from './Typography'
+import {Spacing} from './Spacing'
+import {Buttons} from './Buttons'
+import {Links} from './Links'
+import {Forms} from './Forms'
+import {Dialogs} from './Dialogs'
+import {Breakpoints} from './Breakpoints'
+import {Shadows} from './Shadows'
+import {Icons} from './Icons'
+
+export function Storybook() {
+  const t = useTheme()
+  const setColorMode = useSetColorMode()
+
+  return (
+    <ScrollView>
+      <CenteredView style={[t.atoms.bg]}>
+        <View style={[a.p_xl, a.gap_5xl, {paddingBottom: 200}]}>
+          <View style={[a.flex_row, a.align_start, a.gap_md]}>
+            <Button
+              variant="outline"
+              color="primary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('system')}>
+              System
+            </Button>
+            <Button
+              variant="solid"
+              color="secondary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('light')}>
+              Light
+            </Button>
+            <Button
+              variant="solid"
+              color="secondary"
+              size="small"
+              label='Set theme to "system"'
+              onPress={() => setColorMode('dark')}>
+              Dark
+            </Button>
+          </View>
+
+          <ThemeProvider theme="light">
+            <Theming />
+          </ThemeProvider>
+          <ThemeProvider theme="dim">
+            <Theming />
+          </ThemeProvider>
+          <ThemeProvider theme="dark">
+            <Theming />
+          </ThemeProvider>
+
+          <Typography />
+          <Spacing />
+          <Shadows />
+          <Buttons />
+          <Icons />
+          <Links />
+          <Forms />
+          <Dialogs />
+          <Breakpoints />
+        </View>
+      </CenteredView>
+    </ScrollView>
+  )
+}
diff --git a/src/view/screens/Support.tsx b/src/view/screens/Support.tsx
index 6856f6759..9e7d36ec7 100644
--- a/src/view/screens/Support.tsx
+++ b/src/view/screens/Support.tsx
@@ -34,10 +34,10 @@ export const SupportScreen = (_props: Props) => {
         </Text>
         <Text style={[pal.text, s.p20]}>
           <Trans>
-            The support form has been moved. If you need help, please
+            The support form has been moved. If you need help, please{' '}
             <TextLink
               href={HELP_DESK_URL}
-              text=" click here"
+              text={_(msg`click here`)}
               style={pal.link}
             />{' '}
             or visit {HELP_DESK_URL} to get in touch with us.