about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2024-04-12 14:13:13 -0700
committerGitHub <noreply@github.com>2024-04-12 14:13:13 -0700
commitec5c4929c1c5677d22c923193ce04f3d69b72711 (patch)
treeccc097ea1565ae506e522a76a019bfeb6a63faf3 /src
parent44039c68d678e99f9dc712f1a6dae87aed970ca3 (diff)
downloadvoidsky-ec5c4929c1c5677d22c923193ce04f3d69b72711.tar.zst
PWI improvements (#3489)
* Enable home and feeds on the PWI

* Add global SigninDialog to drive useRequireAuth()

* Tweak desktop styles

* Make the logo in leftnav PWI a clickable home link

* Add label

* Improve dialog on web

* Fix query key

* Go to home after signout from settings screen

* Filter out feeds from the discover listing for logged out users which are known to break without auth

* Update profile header follow/subscribe to give signin prompt

* Show profile feeds tabs on pwi

* Add language selector to account creation footer and pwi left nav desktop

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/Navigation.tsx18
-rw-r--r--src/components/AppLanguageDropdown.tsx67
-rw-r--r--src/components/AppLanguageDropdown.web.tsx62
-rw-r--r--src/components/dialogs/Context.tsx7
-rw-r--r--src/components/dialogs/Signin.tsx99
-rw-r--r--src/screens/Profile/Header/ProfileHeaderLabeler.tsx51
-rw-r--r--src/screens/Profile/Header/ProfileHeaderStandard.tsx1
-rw-r--r--src/screens/Signup/index.tsx11
-rw-r--r--src/state/queries/feed.ts27
-rw-r--r--src/state/session/index.tsx8
-rw-r--r--src/view/com/auth/HomeLoggedOutCTA.tsx170
-rw-r--r--src/view/com/auth/SplashScreen.tsx58
-rw-r--r--src/view/com/auth/SplashScreen.web.tsx56
-rw-r--r--src/view/com/home/HomeHeaderLayout.web.tsx63
-rw-r--r--src/view/com/home/HomeHeaderLayoutMobile.tsx44
-rw-r--r--src/view/screens/Feeds.tsx89
-rw-r--r--src/view/screens/Home.tsx9
-rw-r--r--src/view/screens/Profile.tsx3
-rw-r--r--src/view/screens/Settings/index.tsx10
-rw-r--r--src/view/shell/Drawer.tsx56
-rw-r--r--src/view/shell/NavSignupCard.tsx19
-rw-r--r--src/view/shell/index.tsx39
-rw-r--r--src/view/shell/index.web.tsx28
23 files changed, 518 insertions, 477 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index 070c57960..99c0ebf3c 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -193,7 +193,7 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
       <Stack.Screen
         name="ProfileFeed"
         getComponent={() => ProfileFeedScreen}
-        options={{title: title(msg`Feed`), requireAuth: true}}
+        options={{title: title(msg`Feed`)}}
       />
       <Stack.Screen
         name="ProfileFeedLikedBy"
@@ -331,11 +331,7 @@ function HomeTabNavigator() {
         animationDuration: 250,
         contentStyle: pal.view,
       }}>
-      <HomeTab.Screen
-        name="Home"
-        getComponent={() => HomeScreen}
-        options={{requireAuth: true}}
-      />
+      <HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
       {commonScreens(HomeTab)}
     </HomeTab.Navigator>
   )
@@ -371,11 +367,7 @@ function FeedsTabNavigator() {
         animationDuration: 250,
         contentStyle: pal.view,
       }}>
-      <FeedsTab.Screen
-        name="Feeds"
-        getComponent={() => FeedsScreen}
-        options={{requireAuth: true}}
-      />
+      <FeedsTab.Screen name="Feeds" getComponent={() => FeedsScreen} />
       {commonScreens(FeedsTab as typeof HomeTab)}
     </FeedsTab.Navigator>
   )
@@ -451,7 +443,7 @@ const FlatNavigator = () => {
       <Flat.Screen
         name="Home"
         getComponent={() => HomeScreen}
-        options={{title: title(msg`Home`), requireAuth: true}}
+        options={{title: title(msg`Home`)}}
       />
       <Flat.Screen
         name="Search"
@@ -461,7 +453,7 @@ const FlatNavigator = () => {
       <Flat.Screen
         name="Feeds"
         getComponent={() => FeedsScreen}
-        options={{title: title(msg`Feeds`), requireAuth: true}}
+        options={{title: title(msg`Feeds`)}}
       />
       <Flat.Screen
         name="Notifications"
diff --git a/src/components/AppLanguageDropdown.tsx b/src/components/AppLanguageDropdown.tsx
new file mode 100644
index 000000000..dea9e66fb
--- /dev/null
+++ b/src/components/AppLanguageDropdown.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import {View} from 'react-native'
+import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
+
+import {sanitizeAppLanguageSetting} from '#/locale/helpers'
+import {APP_LANGUAGES} from '#/locale/languages'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
+import {atoms as a, useTheme} from '#/alf'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+
+export function AppLanguageDropdown() {
+  const t = useTheme()
+
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
+  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
+
+  const onChangeAppLanguage = React.useCallback(
+    (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
+      if (!value) return
+      if (sanitizedLang !== value) {
+        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
+      }
+    },
+    [sanitizedLang, setLangPrefs],
+  )
+
+  return (
+    <View style={a.relative}>
+      <RNPickerSelect
+        placeholder={{}}
+        value={sanitizedLang}
+        onValueChange={onChangeAppLanguage}
+        items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
+          label: l.name,
+          value: l.code2,
+          key: l.code2,
+        }))}
+        useNativeAndroidPickerStyle={false}
+        style={{
+          inputAndroid: {
+            color: t.atoms.text_contrast_medium.color,
+            fontSize: 16,
+            paddingRight: 12 + 4,
+          },
+          inputIOS: {
+            color: t.atoms.text.color,
+            fontSize: 16,
+            paddingRight: 12 + 4,
+          },
+        }}
+      />
+
+      <View
+        style={[
+          a.absolute,
+          a.inset_0,
+          {left: 'auto'},
+          {pointerEvents: 'none'},
+          a.align_center,
+          a.justify_center,
+        ]}>
+        <ChevronDown fill={t.atoms.text.color} size="xs" />
+      </View>
+    </View>
+  )
+}
diff --git a/src/components/AppLanguageDropdown.web.tsx b/src/components/AppLanguageDropdown.web.tsx
new file mode 100644
index 000000000..302a30ca6
--- /dev/null
+++ b/src/components/AppLanguageDropdown.web.tsx
@@ -0,0 +1,62 @@
+import React from 'react'
+import {View} from 'react-native'
+
+import {sanitizeAppLanguageSetting} from '#/locale/helpers'
+import {APP_LANGUAGES} from '#/locale/languages'
+import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
+import {atoms as a, useTheme} from '#/alf'
+import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
+import {Text} from '#/components/Typography'
+
+export function AppLanguageDropdown() {
+  const t = useTheme()
+
+  const langPrefs = useLanguagePrefs()
+  const setLangPrefs = useLanguagePrefsApi()
+
+  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
+
+  const onChangeAppLanguage = React.useCallback(
+    (ev: React.ChangeEvent<HTMLSelectElement>) => {
+      const value = ev.target.value
+
+      if (!value) return
+      if (sanitizedLang !== value) {
+        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
+      }
+    },
+    [sanitizedLang, setLangPrefs],
+  )
+
+  return (
+    <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}>
+      <Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
+        {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
+      </Text>
+      <ChevronDown fill={t.atoms.text.color} size="xs" style={a.flex_shrink} />
+
+      <select
+        value={sanitizedLang}
+        onChange={onChangeAppLanguage}
+        style={{
+          cursor: 'pointer',
+          MozAppearance: 'none',
+          WebkitAppearance: 'none',
+          appearance: 'none',
+          position: 'absolute',
+          inset: 0,
+          width: '100%',
+          color: 'transparent',
+          background: 'transparent',
+          border: 0,
+          padding: 0,
+        }}>
+        {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
+          <option key={l.code2} value={l.code2}>
+            {l.name}
+          </option>
+        ))}
+      </select>
+    </View>
+  )
+}
diff --git a/src/components/dialogs/Context.tsx b/src/components/dialogs/Context.tsx
index 87bd5c2ed..c9dff9a99 100644
--- a/src/components/dialogs/Context.tsx
+++ b/src/components/dialogs/Context.tsx
@@ -6,10 +6,12 @@ type Control = Dialog.DialogOuterProps['control']
 
 type ControlsContext = {
   mutedWordsDialogControl: Control
+  signinDialogControl: Control
 }
 
 const ControlsContext = React.createContext({
   mutedWordsDialogControl: {} as Control,
+  signinDialogControl: {} as Control,
 })
 
 export function useGlobalDialogsControlContext() {
@@ -18,9 +20,10 @@ export function useGlobalDialogsControlContext() {
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const mutedWordsDialogControl = Dialog.useDialogControl()
+  const signinDialogControl = Dialog.useDialogControl()
   const ctx = React.useMemo<ControlsContext>(
-    () => ({mutedWordsDialogControl}),
-    [mutedWordsDialogControl],
+    () => ({mutedWordsDialogControl, signinDialogControl}),
+    [mutedWordsDialogControl, signinDialogControl],
   )
 
   return (
diff --git a/src/components/dialogs/Signin.tsx b/src/components/dialogs/Signin.tsx
new file mode 100644
index 000000000..488eb5c73
--- /dev/null
+++ b/src/components/dialogs/Signin.tsx
@@ -0,0 +1,99 @@
+import React from 'react'
+import {View} from 'react-native'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {isNative} from '#/platform/detection'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+import {Logo} from '#/view/icons/Logo'
+import {Logotype} from '#/view/icons/Logotype'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
+import {Text} from '#/components/Typography'
+
+export function SigninDialog() {
+  const {signinDialogControl: control} = useGlobalDialogsControlContext()
+  return (
+    <Dialog.Outer control={control}>
+      <Dialog.Handle />
+      <SigninDialogInner control={control} />
+    </Dialog.Outer>
+  )
+}
+
+function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {gtMobile} = useBreakpoints()
+  const {requestSwitchToAccount} = useLoggedOutViewControls()
+  const closeAllActiveElements = useCloseAllActiveElements()
+
+  const showSignIn = React.useCallback(() => {
+    closeAllActiveElements()
+    requestSwitchToAccount({requestedAccount: 'none'})
+  }, [requestSwitchToAccount, closeAllActiveElements])
+
+  const showCreateAccount = React.useCallback(() => {
+    closeAllActiveElements()
+    requestSwitchToAccount({requestedAccount: 'new'})
+  }, [requestSwitchToAccount, closeAllActiveElements])
+
+  return (
+    <Dialog.ScrollableInner
+      label={_(msg`Sign into Bluesky or create a new account`)}
+      style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}>
+      <View>
+        <View
+          style={[
+            a.flex_row,
+            a.align_center,
+            a.justify_center,
+            a.gap_sm,
+            a.pb_lg,
+          ]}>
+          <Logo width={36} />
+          <View style={{paddingTop: 6}}>
+            <Logotype width={120} fill={t.atoms.text.color} />
+          </View>
+        </View>
+
+        <Text style={[a.text_lg, a.text_center, t.atoms.text, a.pb_2xl]}>
+          <Trans>
+            Sign in or create your account to join the conversation!
+          </Trans>
+        </Text>
+
+        <View style={[a.flex_col, a.gap_md]}>
+          <Button
+            variant="solid"
+            color="primary"
+            size="large"
+            onPress={showCreateAccount}
+            label={_(msg`Create an account`)}>
+            <ButtonText>
+              <Trans>Create an account</Trans>
+            </ButtonText>
+          </Button>
+
+          <Button
+            variant="solid"
+            color="secondary"
+            size="large"
+            onPress={showSignIn}
+            label={_(msg`Sign in`)}>
+            <ButtonText>
+              <Trans>Sign in</Trans>
+            </ButtonText>
+          </Button>
+        </View>
+
+        {isNative && <View style={{height: 10}} />}
+      </View>
+
+      <Dialog.Close />
+    </Dialog.ScrollableInner>
+  )
+}
diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
index d0fd5e20b..b9145822c 100644
--- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
@@ -18,7 +18,7 @@ import {useModalControls} from '#/state/modals'
 import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
 import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
 import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useSession} from '#/state/session'
+import {useRequireAuth, useSession} from '#/state/session'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useHaptics} from 'lib/haptics'
 import {useProfileShadow} from 'state/cache/profile-shadow'
@@ -64,6 +64,7 @@ let ProfileHeaderLabeler = ({
   const {currentAccount, hasSession} = useSession()
   const {openModal} = useModalControls()
   const {track} = useAnalytics()
+  const requireAuth = useRequireAuth()
   const playHaptic = useHaptics()
   const cantSubscribePrompt = Prompt.usePromptControl()
   const isSelf = currentAccount?.did === profile.did
@@ -125,27 +126,32 @@ let ProfileHeaderLabeler = ({
     })
   }, [track, openModal, profile])
 
-  const onPressSubscribe = React.useCallback(async () => {
-    if (!canSubscribe) {
-      cantSubscribePrompt.open()
-      return
-    }
-    try {
-      await toggleSubscription({
-        did: profile.did,
-        subscribe: !isSubscribed,
-      })
-    } catch (e: any) {
-      // setSubscriptionError(e.message)
-      logger.error(`Failed to subscribe to labeler`, {message: e.message})
-    }
-  }, [
-    toggleSubscription,
-    isSubscribed,
-    profile,
-    canSubscribe,
-    cantSubscribePrompt,
-  ])
+  const onPressSubscribe = React.useCallback(
+    () =>
+      requireAuth(async () => {
+        if (!canSubscribe) {
+          cantSubscribePrompt.open()
+          return
+        }
+        try {
+          await toggleSubscription({
+            did: profile.did,
+            subscribe: !isSubscribed,
+          })
+        } catch (e: any) {
+          // setSubscriptionError(e.message)
+          logger.error(`Failed to subscribe to labeler`, {message: e.message})
+        }
+      }),
+    [
+      requireAuth,
+      toggleSubscription,
+      isSubscribed,
+      profile,
+      canSubscribe,
+      cantSubscribePrompt,
+    ],
+  )
 
   const isMe = React.useMemo(
     () => currentAccount?.did === profile.did,
@@ -184,7 +190,6 @@ let ProfileHeaderLabeler = ({
                     ? _(msg`Unsubscribe from this labeler`)
                     : _(msg`Subscribe to this labeler`)
                 }
-                disabled={!hasSession}
                 onPress={onPressSubscribe}>
                 {state => (
                   <View
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index d6c6ff7bd..accef12ed 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -220,7 +220,6 @@ let ProfileHeaderStandard = ({
                     ? _(msg`Unfollow ${profile.handle}`)
                     : _(msg`Follow ${profile.handle}`)
                 }
-                disabled={!hasSession}
                 onPress={
                   profile.viewer?.following ? onPressUnfollow : onPressFollow
                 }
diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx
index 74674b0cb..e17461588 100644
--- a/src/screens/Signup/index.tsx
+++ b/src/screens/Signup/index.tsx
@@ -22,6 +22,7 @@ import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
 import {StepHandle} from '#/screens/Signup/StepHandle'
 import {StepInfo} from '#/screens/Signup/StepInfo'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
 import {Button, ButtonText} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import {InlineLinkText} from '#/components/Link'
@@ -212,10 +213,14 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
 
             <Divider />
 
-            <View style={[a.w_full, a.py_lg]}>
-              <Text style={[t.atoms.text_contrast_medium]}>
+            <View
+              style={[a.w_full, a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
+              <AppLanguageDropdown />
+              <Text style={[t.atoms.text, !gtMobile && a.text_md]}>
                 <Trans>Having trouble?</Trans>{' '}
-                <InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}>
+                <InlineLinkText
+                  to={FEEDBACK_FORM_URL({email: state.email})}
+                  style={[!gtMobile && a.text_md]}>
                   <Trans>Contact support</Trans>
                 </InlineLinkText>
               </Text>
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index c56912491..0d3de8969 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -17,7 +17,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {STALE} from '#/state/queries'
 import {usePreferencesQuery} from '#/state/queries/preferences'
-import {getAgent} from '#/state/session'
+import {getAgent, useSession} from '#/state/session'
 import {router} from '#/routes'
 
 export type FeedSourceFeedInfo = {
@@ -216,17 +216,38 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
   likeCount: 0,
   likeUri: '',
 }
+const DISCOVER_FEED_STUB: FeedSourceInfo = {
+  type: 'feed',
+  displayName: 'Discover',
+  uri: '',
+  route: {
+    href: '/',
+    name: 'Home',
+    params: {},
+  },
+  cid: '',
+  avatar: '',
+  description: new RichText({text: ''}),
+  creatorDid: '',
+  creatorHandle: '',
+  likeCount: 0,
+  likeUri: '',
+}
 
 const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
 
 export function usePinnedFeedsInfos() {
+  const {hasSession} = useSession()
   const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
   const pinnedUris = preferences?.feeds?.pinned ?? []
 
   return useQuery({
     staleTime: STALE.INFINITY,
     enabled: !isLoadingPrefs,
-    queryKey: [pinnedFeedInfosQueryKeyRoot, pinnedUris.join(',')],
+    queryKey: [
+      pinnedFeedInfosQueryKeyRoot,
+      (hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','),
+    ],
     queryFn: async () => {
       let resolved = new Map()
 
@@ -264,7 +285,7 @@ export function usePinnedFeedsInfos() {
       )
 
       // The returned result will have the original order.
-      const result = [FOLLOWING_FEED_STUB]
+      const result = [hasSession ? FOLLOWING_FEED_STUB : DISCOVER_FEED_STUB]
       await Promise.allSettled([feedsPromise, ...listsPromises])
       for (let pinnedUri of pinnedUris) {
         if (resolved.has(pinnedUri)) {
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index 5c7cc1591..b88181ebd 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -15,8 +15,8 @@ import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
 import {PUBLIC_BSKY_AGENT} from '#/state/queries'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useCloseAllActiveElements} from '#/state/util'
+import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
 import {IS_DEV} from '#/env'
 import {emitSessionDropped} from '../events'
 import {readLabelers} from './agent-config'
@@ -702,8 +702,8 @@ export function useSessionApi() {
 
 export function useRequireAuth() {
   const {hasSession} = useSession()
-  const {setShowLoggedOut} = useLoggedOutViewControls()
   const closeAll = useCloseAllActiveElements()
+  const {signinDialogControl} = useGlobalDialogsControlContext()
 
   return React.useCallback(
     (fn: () => void) => {
@@ -711,10 +711,10 @@ export function useRequireAuth() {
         fn()
       } else {
         closeAll()
-        setShowLoggedOut(true)
+        signinDialogControl.open()
       }
     },
-    [hasSession, setShowLoggedOut, closeAll],
+    [hasSession, signinDialogControl, closeAll],
   )
 }
 
diff --git a/src/view/com/auth/HomeLoggedOutCTA.tsx b/src/view/com/auth/HomeLoggedOutCTA.tsx
deleted file mode 100644
index 4c8c35da7..000000000
--- a/src/view/com/auth/HomeLoggedOutCTA.tsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-
-import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
-import {colors, s} from '#/lib/styles'
-import {useLoggedOutViewControls} from '#/state/shell/logged-out'
-import {TextLink} from '../util/Link'
-import {Text} from '../util/text/Text'
-import {ScrollView} from '../util/Views'
-
-export function HomeLoggedOutCTA() {
-  const pal = usePalette('default')
-  const {_} = useLingui()
-  const {isMobile} = useWebMediaQueries()
-  const {requestSwitchToAccount} = useLoggedOutViewControls()
-
-  const showCreateAccount = React.useCallback(() => {
-    requestSwitchToAccount({requestedAccount: 'new'})
-  }, [requestSwitchToAccount])
-
-  const showSignIn = React.useCallback(() => {
-    requestSwitchToAccount({requestedAccount: 'none'})
-  }, [requestSwitchToAccount])
-
-  return (
-    <ScrollView style={styles.container} testID="loggedOutCTA">
-      <View style={[styles.hero, isMobile && styles.heroMobile]}>
-        <Text style={[styles.title, pal.link]}>
-          <Trans>Bluesky</Trans>
-        </Text>
-        <Text
-          style={[
-            styles.subtitle,
-            isMobile && styles.subtitleMobile,
-            pal.textLight,
-          ]}>
-          <Trans>See what's next</Trans>
-        </Text>
-      </View>
-      <View
-        testID="signinOrCreateAccount"
-        style={isMobile ? undefined : styles.btnsDesktop}>
-        <TouchableOpacity
-          testID="createAccountButton"
-          style={[
-            styles.btn,
-            isMobile && styles.btnMobile,
-            {backgroundColor: colors.blue3},
-          ]}
-          onPress={showCreateAccount}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Create new account`)}
-          accessibilityHint={_(
-            msg`Opens flow to create a new Bluesky account`,
-          )}>
-          <Text
-            style={[
-              s.white,
-              styles.btnLabel,
-              isMobile && styles.btnLabelMobile,
-            ]}>
-            <Trans>Create a new account</Trans>
-          </Text>
-        </TouchableOpacity>
-        <TouchableOpacity
-          testID="signInButton"
-          style={[styles.btn, isMobile && styles.btnMobile, pal.btn]}
-          onPress={showSignIn}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Sign in`)}
-          accessibilityHint={_(
-            msg`Opens flow to sign into your existing Bluesky account`,
-          )}>
-          <Text
-            style={[
-              pal.text,
-              styles.btnLabel,
-              isMobile && styles.btnLabelMobile,
-            ]}>
-            <Trans>Sign in</Trans>
-          </Text>
-        </TouchableOpacity>
-      </View>
-
-      <View style={[styles.footer, pal.view, pal.border]}>
-        <TextLink
-          type="2xl"
-          href="https://bsky.social"
-          text={_(msg`Business`)}
-          style={[styles.footerLink, pal.link]}
-        />
-        <TextLink
-          type="2xl"
-          href="https://bsky.social/about/blog"
-          text={_(msg`Blog`)}
-          style={[styles.footerLink, pal.link]}
-        />
-        <TextLink
-          type="2xl"
-          href="https://bsky.social/about/join"
-          text={_(msg`Jobs`)}
-          style={[styles.footerLink, pal.link]}
-        />
-      </View>
-    </ScrollView>
-  )
-}
-
-const styles = StyleSheet.create({
-  container: {
-    height: '100%',
-  },
-  hero: {
-    justifyContent: 'center',
-    paddingTop: 100,
-    paddingBottom: 30,
-  },
-  heroMobile: {
-    paddingBottom: 50,
-  },
-  title: {
-    textAlign: 'center',
-    fontSize: 68,
-    fontWeight: 'bold',
-  },
-  subtitle: {
-    textAlign: 'center',
-    fontSize: 48,
-    fontWeight: 'bold',
-  },
-  subtitleMobile: {
-    fontSize: 42,
-  },
-  btnsDesktop: {
-    flexDirection: 'row',
-    justifyContent: 'center',
-    gap: 20,
-    marginHorizontal: 20,
-  },
-  btn: {
-    borderRadius: 32,
-    width: 230,
-    paddingVertical: 12,
-    marginBottom: 20,
-  },
-  btnMobile: {
-    flex: 1,
-    width: 'auto',
-    marginHorizontal: 20,
-    paddingVertical: 16,
-  },
-  btnLabel: {
-    textAlign: 'center',
-    fontSize: 18,
-  },
-  btnLabelMobile: {
-    textAlign: 'center',
-    fontSize: 21,
-  },
-
-  footer: {
-    flexDirection: 'row',
-    gap: 20,
-    justifyContent: 'center',
-  },
-  footerLink: {},
-})
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index 763b01dfa..8eac1ab82 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -1,19 +1,15 @@
 import React from 'react'
 import {View} from 'react-native'
-import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {sanitizeAppLanguageSetting} from '#/locale/helpers'
-import {APP_LANGUAGES} from '#/locale/languages'
-import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {atoms as a, useTheme} from '#/alf'
+import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
 import {Button, ButtonText} from '#/components/Button'
-import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
 import {Text} from '#/components/Typography'
 import {CenteredView} from '../util/Views'
 
@@ -27,22 +23,8 @@ export const SplashScreen = ({
   const t = useTheme()
   const {_} = useLingui()
 
-  const langPrefs = useLanguagePrefs()
-  const setLangPrefs = useLanguagePrefsApi()
   const insets = useSafeAreaInsets()
 
-  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
-
-  const onChangeAppLanguage = React.useCallback(
-    (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
-      if (!value) return
-      if (sanitizedLang !== value) {
-        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
-      }
-    },
-    [sanitizedLang, setLangPrefs],
-  )
-
   return (
     <CenteredView style={[a.h_full, a.flex_1]}>
       <ErrorBoundary>
@@ -99,43 +81,7 @@ export const SplashScreen = ({
             a.justify_center,
             a.align_center,
           ]}>
-          <View style={a.relative}>
-            <RNPickerSelect
-              placeholder={{}}
-              value={sanitizedLang}
-              onValueChange={onChangeAppLanguage}
-              items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
-                label: l.name,
-                value: l.code2,
-                key: l.code2,
-              }))}
-              useNativeAndroidPickerStyle={false}
-              style={{
-                inputAndroid: {
-                  color: t.atoms.text_contrast_medium.color,
-                  fontSize: 16,
-                  paddingRight: 12 + 4,
-                },
-                inputIOS: {
-                  color: t.atoms.text.color,
-                  fontSize: 16,
-                  paddingRight: 12 + 4,
-                },
-              }}
-            />
-
-            <View
-              style={[
-                a.absolute,
-                a.inset_0,
-                {left: 'auto'},
-                {pointerEvents: 'none'},
-                a.align_center,
-                a.justify_center,
-              ]}>
-              <ChevronDown fill={t.atoms.text.color} size="xs" />
-            </View>
-          </View>
+          <AppLanguageDropdown />
         </View>
         <View style={{height: insets.bottom}} />
       </ErrorBoundary>
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index 7a2ee16cf..f905e1e8d 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -4,16 +4,13 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {sanitizeAppLanguageSetting} from '#/locale/helpers'
-import {APP_LANGUAGES} from '#/locale/languages'
-import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {Logo} from '#/view/icons/Logo'
 import {Logotype} from '#/view/icons/Logotype'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {atoms as a, useTheme} from '#/alf'
+import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
 import {Button, ButtonText} from '#/components/Button'
-import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
 import {CenteredView} from '../util/Views'
@@ -131,23 +128,6 @@ export const SplashScreen = ({
 function Footer() {
   const t = useTheme()
 
-  const langPrefs = useLanguagePrefs()
-  const setLangPrefs = useLanguagePrefsApi()
-
-  const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
-
-  const onChangeAppLanguage = React.useCallback(
-    (ev: React.ChangeEvent<HTMLSelectElement>) => {
-      const value = ev.target.value
-
-      if (!value) return
-      if (sanitizedLang !== value) {
-        setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
-      }
-    },
-    [sanitizedLang, setLangPrefs],
-  )
-
   return (
     <View
       style={[
@@ -174,39 +154,7 @@ function Footer() {
 
       <View style={a.flex_1} />
 
-      <View style={[a.flex_row, a.gap_sm, a.align_center, a.flex_shrink]}>
-        <Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
-          {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
-        </Text>
-        <ChevronDown
-          fill={t.atoms.text.color}
-          size="xs"
-          style={a.flex_shrink}
-        />
-
-        <select
-          value={sanitizedLang}
-          onChange={onChangeAppLanguage}
-          style={{
-            cursor: 'pointer',
-            MozAppearance: 'none',
-            WebkitAppearance: 'none',
-            appearance: 'none',
-            position: 'absolute',
-            inset: 0,
-            width: '100%',
-            color: 'transparent',
-            background: 'transparent',
-            border: 0,
-            padding: 0,
-          }}>
-          {APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
-            <option key={l.code2} value={l.code2}>
-              {l.name}
-            </option>
-          ))}
-        </select>
-      </View>
+      <AppLanguageDropdown />
     </View>
   )
 }
diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx
index 9818b56f6..644d4cab6 100644
--- a/src/view/com/home/HomeHeaderLayout.web.tsx
+++ b/src/view/com/home/HomeHeaderLayout.web.tsx
@@ -1,20 +1,22 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import Animated from 'react-native-reanimated'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
-import {Logo} from '#/view/icons/Logo'
-import {Link} from '../util/Link'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
 import {CogIcon} from '#/lib/icons'
-import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+import {useSession} from '#/state/session'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {Logo} from '#/view/icons/Logo'
+import {Link} from '../util/Link'
+import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
 
 export function HomeHeaderLayout(props: {
   children: React.ReactNode
@@ -38,32 +40,35 @@ function HomeHeaderLayoutDesktopAndTablet({
   const pal = usePalette('default')
   const {headerMinimalShellTransform} = useMinimalShellMode()
   const {headerHeight} = useShellLayout()
+  const {hasSession} = useSession()
   const {_} = useLingui()
 
   return (
     <>
-      <View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
-        <Link
-          href="/settings/following-feed"
-          hitSlop={10}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Following Feed Preferences`)}
-          accessibilityHint="">
-          <FontAwesomeIcon
-            icon="sliders"
-            style={pal.textLight as FontAwesomeIconStyle}
-          />
-        </Link>
-        <Logo width={28} />
-        <Link
-          href="/settings/saved-feeds"
-          hitSlop={10}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Edit Saved Feeds`)}
-          accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
-          <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
-        </Link>
-      </View>
+      {hasSession && (
+        <View style={[pal.view, pal.border, styles.bar, styles.topBar]}>
+          <Link
+            href="/settings/following-feed"
+            hitSlop={10}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Following Feed Preferences`)}
+            accessibilityHint="">
+            <FontAwesomeIcon
+              icon="sliders"
+              style={pal.textLight as FontAwesomeIconStyle}
+            />
+          </Link>
+          <Logo width={28} />
+          <Link
+            href="/settings/saved-feeds"
+            hitSlop={10}
+            accessibilityRole="button"
+            accessibilityLabel={_(msg`Edit Saved Feeds`)}
+            accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
+            <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
+          </Link>
+        </View>
+      )}
       {tabBarAnchor}
       <Animated.View
         onLayout={e => {
diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx
index d7b7231c6..78fa9af86 100644
--- a/src/view/com/home/HomeHeaderLayoutMobile.tsx
+++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx
@@ -1,23 +1,24 @@
 import React from 'react'
 import {StyleSheet, TouchableOpacity, View} from 'react-native'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Link} from '../util/Link'
+import Animated from 'react-native-reanimated'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
-import {HITSLOP_10} from 'lib/constants'
-import Animated from 'react-native-reanimated'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+
+import {useSession} from '#/state/session'
 import {useSetDrawerOpen} from '#/state/shell/drawer-open'
 import {useShellLayout} from '#/state/shell/shell-layout'
+import {HITSLOP_10} from 'lib/constants'
+import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
+import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
 import {Logo} from '#/view/icons/Logo'
-
-import {IS_DEV} from '#/env'
 import {atoms} from '#/alf'
-import {Link as Link2} from '#/components/Link'
 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPalette} from '#/components/icons/ColorPalette'
+import {Link as Link2} from '#/components/Link'
+import {IS_DEV} from '#/env'
+import {Link} from '../util/Link'
 
 export function HomeHeaderLayoutMobile({
   children,
@@ -30,6 +31,7 @@ export function HomeHeaderLayoutMobile({
   const setDrawerOpen = useSetDrawerOpen()
   const {headerHeight} = useShellLayout()
   const {headerMinimalShellTransform} = useMinimalShellMode()
+  const {hasSession} = useSession()
 
   const onPressAvi = React.useCallback(() => {
     setDrawerOpen(true)
@@ -76,18 +78,20 @@ export function HomeHeaderLayoutMobile({
               <ColorPalette size="md" />
             </Link2>
           )}
-          <Link
-            testID="viewHeaderHomeFeedPrefsBtn"
-            href="/settings/following-feed"
-            hitSlop={HITSLOP_10}
-            accessibilityRole="button"
-            accessibilityLabel={_(msg`Following Feed Preferences`)}
-            accessibilityHint="">
-            <FontAwesomeIcon
-              icon="sliders"
-              style={pal.textLight as FontAwesomeIconStyle}
-            />
-          </Link>
+          {hasSession && (
+            <Link
+              testID="viewHeaderHomeFeedPrefsBtn"
+              href="/settings/following-feed"
+              hitSlop={HITSLOP_10}
+              accessibilityRole="button"
+              accessibilityLabel={_(msg`Following Feed Preferences`)}
+              accessibilityHint="">
+              <FontAwesomeIcon
+                icon="sliders"
+                style={pal.textLight as FontAwesomeIconStyle}
+              />
+            </Link>
+          )}
         </View>
       </View>
       {children}
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 2e3bf08db..e64ab08df 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,52 +1,53 @@
 import React from 'react'
 import {
   ActivityIndicator,
-  StyleSheet,
-  View,
   type FlatList,
   Pressable,
+  StyleSheet,
+  View,
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {FAB} from 'view/com/util/fab/FAB'
-import {Link} from 'view/com/util/Link'
-import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ComposeIcon2, CogIcon, MagnifyingGlassIcon2} from 'lib/icons'
-import {s} from 'lib/styles'
-import {atoms as a, useTheme} from '#/alf'
-import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {
-  LoadingPlaceholder,
-  FeedFeedLoadingPlaceholder,
-} from 'view/com/util/LoadingPlaceholder'
-import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
-import debounce from 'lodash.debounce'
-import {Text} from 'view/com/util/text/Text'
-import {List} from 'view/com/util/List'
-import {useFocusEffect} from '@react-navigation/native'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {Trans, msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useFocusEffect} from '@react-navigation/native'
+import debounce from 'lodash.debounce'
+
+import {isNative, isWeb} from '#/platform/detection'
 import {
+  getAvatarTypeFromUri,
   useFeedSourceInfoQuery,
   useGetPopularFeedsQuery,
   useSearchPopularFeedsMutation,
-  getAvatarTypeFromUri,
 } from '#/state/queries/feed'
-import {cleanError} from 'lib/strings/errors'
-import {useComposerControls} from '#/state/shell/composer'
+import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
-import {isNative, isWeb} from '#/platform/detection'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useComposerControls} from '#/state/shell/composer'
 import {HITSLOP_10} from 'lib/constants'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {CogIcon, ComposeIcon2, MagnifyingGlassIcon2} from 'lib/icons'
+import {FeedsTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {cleanError} from 'lib/strings/errors'
+import {s} from 'lib/styles'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
+import {FAB} from 'view/com/util/fab/FAB'
+import {SearchInput, SearchInputRef} from 'view/com/util/forms/SearchInput'
+import {Link} from 'view/com/util/Link'
+import {List} from 'view/com/util/List'
+import {
+  FeedFeedLoadingPlaceholder,
+  LoadingPlaceholder,
+} from 'view/com/util/LoadingPlaceholder'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {atoms as a, useTheme} from '#/alf'
 import {IconCircle} from '#/components/IconCircle'
-import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
 import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
+import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
 
 type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
 
@@ -100,6 +101,22 @@ type FlatlistSlice =
       key: string
     }
 
+// HACK
+// the protocol doesn't yet tell us which feeds are personalized
+// this list is used to filter out feed recommendations from logged out users
+// for the ones we know need it
+// -prf
+const KNOWN_AUTHED_ONLY_FEEDS = [
+  'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends', // popular with friends, by bsky.app
+  'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/mutuals', // mutuals, by skyfeed
+  'at://did:plc:tenurhgjptubkk5zf5qhi3og/app.bsky.feed.generator/only-posts', // only posts, by skyfeed
+  'at://did:plc:wzsilnxf24ehtmmc3gssy5bu/app.bsky.feed.generator/mentions', // mentions, by flicknow
+  'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/bangers', // my bangers, by jaz
+  'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/mutuals', // mutuals, by bluesky
+  'at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/my-followers', // followers, by jaz
+  'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/followpics', // the gram, by why
+]
+
 export function FeedsScreen(_props: Props) {
   const pal = usePalette('default')
   const {openComposer} = useComposerControls()
@@ -299,7 +316,15 @@ export function FeedsScreen(_props: Props) {
             for (const page of popularFeeds.pages || []) {
               slices = slices.concat(
                 page.feeds
-                  .filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
+                  .filter(feed => {
+                    if (
+                      !hasSession &&
+                      KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri)
+                    ) {
+                      return false
+                    }
+                    return !preferences?.feeds?.saved.includes(feed.uri)
+                  })
                   .map(feed => ({
                     key: `popularFeed:${feed.uri}`,
                     type: 'popularFeed',
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 39bdac669..7a2a88265 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -2,6 +2,7 @@ import React from 'react'
 import {ActivityIndicator, AppState, StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 
+import {PROD_DEFAULT_FEED} from '#/lib/constants'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {useSetTitle} from '#/lib/hooks/useSetTitle'
 import {logEvent, LogEvents, useGate} from '#/lib/statsig/statsig'
@@ -19,7 +20,6 @@ import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
-import {HomeLoggedOutCTA} from '../com/auth/HomeLoggedOutCTA'
 import {HomeHeader} from '../com/home/HomeHeader'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
@@ -231,7 +231,12 @@ function HomeScreenReady({
       onPageSelected={onPageSelected}
       onPageScrollStateChanged={onPageScrollStateChanged}
       renderTabBar={renderTabBar}>
-      <HomeLoggedOutCTA />
+      <FeedPage
+        testID="customFeedPage"
+        isPageFocused
+        feed={`feedgen|${PROD_DEFAULT_FEED('whats-hot')}`}
+        renderEmptyState={renderCustomFeedEmptyState}
+      />
     </Pager>
   )
 }
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index c391f8050..f71e1330e 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -184,8 +184,7 @@ function ProfileScreenLoaded({
   const showRepliesTab = hasSession
   const showMediaTab = !hasLabeler
   const showLikesTab = isMe
-  const showFeedsTab =
-    hasSession && (isMe || (profile.associated?.feedgens || 0) > 0)
+  const showFeedsTab = isMe || (profile.associated?.feedgens || 0) > 0
   const showListsTab =
     hasSession && (isMe || (profile.associated?.lists || 0) > 0)
 
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 8a7fa5e71..b97faafad 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -71,6 +71,7 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
 import {ScrollView} from 'view/com/util/Views'
 import {useDialogControl} from '#/components/Dialog'
 import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
+import {navigate, resetToTab} from '#/Navigation'
 import {ExportCarDialog} from './ExportCarDialog'
 
 function SettingsAccountCard({account}: {account: SessionAccount}) {
@@ -104,7 +105,14 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
         <TouchableOpacity
           testID="signOutBtn"
           onPress={() => {
-            logout('Settings')
+            if (isNative) {
+              logout('Settings')
+              resetToTab('HomeTab')
+            } else {
+              navigate('Home').then(() => {
+                logout('Settings')
+              })
+            }
           }}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Sign out`)}
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 1bf5647f6..a7342179d 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -9,49 +9,49 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {useNavigation, StackActions} from '@react-navigation/native'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {s, colors} from 'lib/styles'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {StackActions, useNavigation} from '@react-navigation/native'
+
+import {emitSoftReset} from '#/state/events'
+import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {useProfileQuery} from '#/state/queries/profile'
+import {SessionAccount, useSession} from '#/state/session'
+import {useSetDrawerOpen} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
 import {FEEDBACK_FORM_URL, HELP_DESK_URL} from 'lib/constants'
+import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
+import {usePalette} from 'lib/hooks/usePalette'
 import {
-  HomeIcon,
-  HomeIconSolid,
   BellIcon,
   BellIconSolid,
-  UserIcon,
   CogIcon,
+  HandIcon,
+  HashtagIcon,
+  HomeIcon,
+  HomeIconSolid,
+  ListIcon,
   MagnifyingGlassIcon2,
   MagnifyingGlassIcon2Solid,
+  UserIcon,
   UserIconSolid,
-  HashtagIcon,
-  ListIcon,
-  HandIcon,
 } from 'lib/icons'
-import {UserAvatar} from 'view/com/util/UserAvatar'
-import {Text} from 'view/com/util/text/Text'
-import {useTheme} from 'lib/ThemeContext'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {pluralize} from 'lib/strings/helpers'
 import {getTabState, TabState} from 'lib/routes/helpers'
 import {NavigationProp} from 'lib/routes/types'
-import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
+import {pluralize} from 'lib/strings/helpers'
+import {colors, s} from 'lib/styles'
+import {useTheme} from 'lib/ThemeContext'
 import {isWeb} from 'platform/detection'
-import {formatCountShortOnly} from 'view/com/util/numeric/format'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useSetDrawerOpen} from '#/state/shell'
-import {useSession, SessionAccount} from '#/state/session'
-import {useProfileQuery} from '#/state/queries/profile'
-import {useUnreadNotifications} from '#/state/queries/notifications/unread'
-import {emitSoftReset} from '#/state/events'
 import {NavSignupCard} from '#/view/shell/NavSignupCard'
-import {TextLink} from '../com/util/Link'
-
+import {formatCountShortOnly} from 'view/com/util/numeric/format'
+import {Text} from 'view/com/util/text/Text'
+import {UserAvatar} from 'view/com/util/UserAvatar'
 import {useTheme as useAlfTheme} from '#/alf'
+import {TextLink} from '../com/util/Link'
 
 let DrawerProfileCard = ({
   account,
@@ -246,7 +246,11 @@ let DrawerContent = ({}: {}): React.ReactNode => {
               <SettingsMenuItem onPress={onPressSettings} />
             </>
           ) : (
-            <SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
+            <>
+              <HomeMenuItem isActive={isAtHome} onPress={onPressHome} />
+              <FeedsMenuItem isActive={isAtFeeds} onPress={onPressMyFeeds} />
+              <SearchMenuItem isActive={isAtSearch} onPress={onPressSearch} />
+            </>
           )}
 
           <View style={styles.smallSpacer} />
diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx
index 83d141498..aa807f0cc 100644
--- a/src/view/shell/NavSignupCard.tsx
+++ b/src/view/shell/NavSignupCard.tsx
@@ -3,13 +3,16 @@ import {View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {s} from 'lib/styles'
-import {usePalette} from 'lib/hooks/usePalette'
-import {Text} from '#/view/com/util/text/Text'
-import {Button} from '#/view/com/util/forms/Button'
 import {useLoggedOutViewControls} from '#/state/shell/logged-out'
 import {useCloseAllActiveElements} from '#/state/util'
+import {usePalette} from 'lib/hooks/usePalette'
+import {s} from 'lib/styles'
+import {Button} from '#/view/com/util/forms/Button'
+import {Text} from '#/view/com/util/text/Text'
 import {Logo} from '#/view/icons/Logo'
+import {atoms as a} from '#/alf'
+import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
+import {Link} from '#/components/Link'
 
 let NavSignupCard = ({}: {}): React.ReactNode => {
   const {_} = useLingui()
@@ -35,7 +38,9 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
         paddingTop: 6,
         marginBottom: 24,
       }}>
-      <Logo width={48} />
+      <Link to="/" label="Bluesky - Home">
+        <Logo width={48} />
+      </Link>
 
       <View style={{paddingTop: 18}}>
         <Text type="md-bold" style={[pal.text]}>
@@ -62,6 +67,10 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
           </Text>
         </Button>
       </View>
+
+      <View style={[a.pt_2xl, a.w_full]}>
+        <AppLanguageDropdown />
+      </View>
     </View>
   )
 }
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index f29183095..c554112ed 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -1,37 +1,39 @@
 import React from 'react'
-import {StatusBar} from 'expo-status-bar'
 import {
+  BackHandler,
   DimensionValue,
   StyleSheet,
   useWindowDimensions,
   View,
-  BackHandler,
 } from 'react-native'
-import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import {Drawer} from 'react-native-drawer-layout'
+import Animated from 'react-native-reanimated'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {StatusBar} from 'expo-status-bar'
 import {useNavigationState} from '@react-navigation/native'
-import {ModalsContainer} from 'view/com/modals/Modal'
-import {Lightbox} from 'view/com/lightbox/Lightbox'
-import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
-import {DrawerContent} from './Drawer'
-import {Composer} from './Composer'
-import {useTheme} from 'lib/ThemeContext'
-import {usePalette} from 'lib/hooks/usePalette'
-import {RoutesContainer, TabsNavigator} from '../../Navigation'
-import {isStateAtTabRoot} from 'lib/routes/helpers'
+
+import {useSession} from '#/state/session'
 import {
   useIsDrawerOpen,
-  useSetDrawerOpen,
   useIsDrawerSwipeDisabled,
+  useSetDrawerOpen,
 } from '#/state/shell'
-import {isAndroid} from 'platform/detection'
-import {useSession} from '#/state/session'
 import {useCloseAnyActiveElement} from '#/state/util'
+import {usePalette} from 'lib/hooks/usePalette'
 import * as notifications from 'lib/notifications/notifications'
-import {Outlet as PortalOutlet} from '#/components/Portal'
-import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
+import {isStateAtTabRoot} from 'lib/routes/helpers'
+import {useTheme} from 'lib/ThemeContext'
+import {isAndroid} from 'platform/detection'
 import {useDialogStateContext} from 'state/dialogs'
-import Animated from 'react-native-reanimated'
+import {Lightbox} from 'view/com/lightbox/Lightbox'
+import {ModalsContainer} from 'view/com/modals/Modal'
+import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
+import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
+import {SigninDialog} from '#/components/dialogs/Signin'
+import {Outlet as PortalOutlet} from '#/components/Portal'
+import {RoutesContainer, TabsNavigator} from '../../Navigation'
+import {Composer} from './Composer'
+import {DrawerContent} from './Drawer'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -101,6 +103,7 @@ function ShellInner() {
       <Composer winHeight={winDim.height} />
       <ModalsContainer />
       <MutedWordsDialog />
+      <SigninDialog />
       <Lightbox />
       <PortalOutlet />
     </>
diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx
index 02993ac46..51fb4a0a1 100644
--- a/src/view/shell/index.web.tsx
+++ b/src/view/shell/index.web.tsx
@@ -1,24 +1,25 @@
 import React, {useEffect} from 'react'
-import {View, StyleSheet, TouchableOpacity} from 'react-native'
-import {useNavigation} from '@react-navigation/native'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
 
-import {ErrorBoundary} from '../com/util/ErrorBoundary'
+import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
+import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
+import {useCloseAllActiveElements} from '#/state/util'
+import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
+import {NavigationProp} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
+import {SigninDialog} from '#/components/dialogs/Signin'
+import {Outlet as PortalOutlet} from '#/components/Portal'
+import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
+import {FlatNavigator, RoutesContainer} from '../../Navigation'
 import {Lightbox} from '../com/lightbox/Lightbox'
 import {ModalsContainer} from '../com/modals/Modal'
+import {ErrorBoundary} from '../com/util/ErrorBoundary'
 import {Composer} from './Composer.web'
-import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
-import {s, colors} from 'lib/styles'
-import {RoutesContainer, FlatNavigator} from '../../Navigation'
 import {DrawerContent} from './Drawer'
-import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
-import {NavigationProp} from 'lib/routes/types'
-import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
-import {useCloseAllActiveElements} from '#/state/util'
-import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
-import {Outlet as PortalOutlet} from '#/components/Portal'
-import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -45,6 +46,7 @@ function ShellInner() {
       <Composer winHeight={0} />
       <ModalsContainer />
       <MutedWordsDialog />
+      <SigninDialog />
       <Lightbox />
       <PortalOutlet />