about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/auth/LoggedOut.tsx3
-rw-r--r--src/view/com/auth/SplashScreen.tsx32
-rw-r--r--src/view/com/auth/SplashScreen.web.tsx110
-rw-r--r--src/view/com/auth/withAuthRequired.tsx17
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx10
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx6
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.web.tsx58
-rw-r--r--src/view/screens/Feeds.tsx737
-rw-r--r--src/view/screens/Home.tsx63
-rw-r--r--src/view/screens/PostLikedBy.tsx37
-rw-r--r--src/view/screens/PostRepostedBy.tsx37
-rw-r--r--src/view/screens/PostThread.tsx145
-rw-r--r--src/view/screens/Profile.tsx131
-rw-r--r--src/view/screens/ProfileFeed.tsx3
-rw-r--r--src/view/screens/ProfileFeedLikedBy.tsx37
-rw-r--r--src/view/screens/ProfileFollowers.tsx35
-rw-r--r--src/view/screens/ProfileFollows.tsx35
-rw-r--r--src/view/screens/SavedFeeds.tsx16
18 files changed, 837 insertions, 675 deletions
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index 3505e86af..daafa60a3 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -15,7 +15,7 @@ enum ScreenState {
   S_CreateAccount,
 }
 
-export function LoggedOut() {
+export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
   const pal = usePalette('default')
   const setMinimalShellMode = useSetMinimalShellMode()
   const {screen} = useAnalytics()
@@ -31,6 +31,7 @@ export function LoggedOut() {
   if (screenState === ScreenState.S_LoginOrCreateAccount) {
     return (
       <SplashScreen
+        onDismiss={onDismiss}
         onPressSignin={() => setScreenState(ScreenState.S_Login)}
         onPressCreateAccount={() => setScreenState(ScreenState.S_CreateAccount)}
       />
diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx
index 05e72a2e6..2c968aef4 100644
--- a/src/view/com/auth/SplashScreen.tsx
+++ b/src/view/com/auth/SplashScreen.tsx
@@ -1,5 +1,12 @@
 import React from 'react'
-import {SafeAreaView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {
+  SafeAreaView,
+  StyleSheet,
+  TouchableOpacity,
+  Pressable,
+  View,
+} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from 'view/com/util/text/Text'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
 import {s, colors} from 'lib/styles'
@@ -9,9 +16,11 @@ import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 export const SplashScreen = ({
+  onDismiss,
   onPressSignin,
   onPressCreateAccount,
 }: {
+  onDismiss?: () => void
   onPressSignin: () => void
   onPressCreateAccount: () => void
 }) => {
@@ -20,6 +29,27 @@ export const SplashScreen = ({
 
   return (
     <CenteredView style={[styles.container, pal.view]}>
+      {onDismiss && (
+        <Pressable
+          accessibilityRole="button"
+          style={{
+            position: 'absolute',
+            top: 20,
+            right: 20,
+            padding: 20,
+            zIndex: 100,
+          }}
+          onPress={onDismiss}>
+          <FontAwesomeIcon
+            icon="x"
+            size={24}
+            style={{
+              color: String(pal.text.color),
+            }}
+          />
+        </Pressable>
+      )}
+
       <SafeAreaView testID="noSessionView" style={styles.container}>
         <ErrorBoundary>
           <View style={styles.hero}>
diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx
index f10dc4f98..08cf701da 100644
--- a/src/view/com/auth/SplashScreen.web.tsx
+++ b/src/view/com/auth/SplashScreen.web.tsx
@@ -1,5 +1,6 @@
 import React from 'react'
-import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import {StyleSheet, TouchableOpacity, View, Pressable} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from 'view/com/util/text/Text'
 import {TextLink} from '../util/Link'
 import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -11,9 +12,11 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {Trans} from '@lingui/macro'
 
 export const SplashScreen = ({
+  onDismiss,
   onPressSignin,
   onPressCreateAccount,
 }: {
+  onDismiss?: () => void
   onPressSignin: () => void
   onPressCreateAccount: () => void
 }) => {
@@ -23,47 +26,70 @@ export const SplashScreen = ({
   const isMobileWeb = isWeb && isTabletOrMobile
 
   return (
-    <CenteredView style={[styles.container, pal.view]}>
-      <View
-        testID="noSessionView"
-        style={[
-          styles.containerInner,
-          isMobileWeb && styles.containerInnerMobile,
-          pal.border,
-        ]}>
-        <ErrorBoundary>
-          <Text style={isMobileWeb ? styles.titleMobile : styles.title}>
-            Bluesky
-          </Text>
-          <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
-            See what's next
-          </Text>
-          <View testID="signinOrCreateAccount" style={styles.btns}>
-            <TouchableOpacity
-              testID="createAccountButton"
-              style={[styles.btn, {backgroundColor: colors.blue3}]}
-              onPress={onPressCreateAccount}
-              // TODO: web accessibility
-              accessibilityRole="button">
-              <Text style={[s.white, styles.btnLabel]}>
-                Create a new account
-              </Text>
-            </TouchableOpacity>
-            <TouchableOpacity
-              testID="signInButton"
-              style={[styles.btn, pal.btn]}
-              onPress={onPressSignin}
-              // TODO: web accessibility
-              accessibilityRole="button">
-              <Text style={[pal.text, styles.btnLabel]}>
-                <Trans>Sign In</Trans>
-              </Text>
-            </TouchableOpacity>
-          </View>
-        </ErrorBoundary>
-      </View>
-      <Footer styles={styles} />
-    </CenteredView>
+    <>
+      {onDismiss && (
+        <Pressable
+          accessibilityRole="button"
+          style={{
+            position: 'absolute',
+            top: 20,
+            right: 20,
+            padding: 20,
+            zIndex: 100,
+          }}
+          onPress={onDismiss}>
+          <FontAwesomeIcon
+            icon="x"
+            size={24}
+            style={{
+              color: String(pal.text.color),
+            }}
+          />
+        </Pressable>
+      )}
+
+      <CenteredView style={[styles.container, pal.view]}>
+        <View
+          testID="noSessionView"
+          style={[
+            styles.containerInner,
+            isMobileWeb && styles.containerInnerMobile,
+            pal.border,
+          ]}>
+          <ErrorBoundary>
+            <Text style={isMobileWeb ? styles.titleMobile : styles.title}>
+              Bluesky
+            </Text>
+            <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
+              See what's next
+            </Text>
+            <View testID="signinOrCreateAccount" style={styles.btns}>
+              <TouchableOpacity
+                testID="createAccountButton"
+                style={[styles.btn, {backgroundColor: colors.blue3}]}
+                onPress={onPressCreateAccount}
+                // TODO: web accessibility
+                accessibilityRole="button">
+                <Text style={[s.white, styles.btnLabel]}>
+                  Create a new account
+                </Text>
+              </TouchableOpacity>
+              <TouchableOpacity
+                testID="signInButton"
+                style={[styles.btn, pal.btn]}
+                onPress={onPressSignin}
+                // TODO: web accessibility
+                accessibilityRole="button">
+                <Text style={[pal.text, styles.btnLabel]}>
+                  <Trans>Sign In</Trans>
+                </Text>
+              </TouchableOpacity>
+            </View>
+          </ErrorBoundary>
+        </View>
+        <Footer styles={styles} />
+      </CenteredView>
+    </>
   )
 }
 
diff --git a/src/view/com/auth/withAuthRequired.tsx b/src/view/com/auth/withAuthRequired.tsx
index 5fd89a8bd..7a9138545 100644
--- a/src/view/com/auth/withAuthRequired.tsx
+++ b/src/view/com/auth/withAuthRequired.tsx
@@ -13,18 +13,33 @@ import {usePalette} from 'lib/hooks/usePalette'
 import {STATUS_PAGE_URL} from 'lib/constants'
 import {useOnboardingState} from '#/state/shell'
 import {useSession} from '#/state/session'
+import {
+  useLoggedOutView,
+  useLoggedOutViewControls,
+} from '#/state/shell/logged-out'
+import {IS_PROD} from '#/env'
 
 export const withAuthRequired = <P extends object>(
   Component: React.ComponentType<P>,
+  options: {
+    isPublic?: boolean // TODO(pwi) need to enable in TF somehow
+  } = {},
 ): React.FC<P> =>
   function AuthRequired(props: P) {
     const {isInitialLoad, hasSession} = useSession()
     const onboardingState = useOnboardingState()
+    const {showLoggedOut} = useLoggedOutView()
+    const {setShowLoggedOut} = useLoggedOutViewControls()
+
     if (isInitialLoad) {
       return <Loading />
     }
     if (!hasSession) {
-      return <LoggedOut />
+      if (showLoggedOut) {
+        return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
+      } else if (!options?.isPublic || IS_PROD) {
+        return <LoggedOut />
+      }
     }
     if (onboardingState.isActive) {
       return <Onboarding />
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 120aecf45..e548c45f7 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -25,6 +25,7 @@ import {
 } from '#/state/queries/post'
 import {useComposerControls} from '#/state/shell/composer'
 import {Shadow} from '#/state/cache/types'
+import {useRequireAuth} from '#/state/session'
 
 export function PostCtrls({
   big,
@@ -46,6 +47,7 @@ export function PostCtrls({
   const postUnlikeMutation = usePostUnlikeMutation()
   const postRepostMutation = usePostRepostMutation()
   const postUnrepostMutation = usePostUnrepostMutation()
+  const requireAuth = useRequireAuth()
 
   const defaultCtrlColor = React.useMemo(
     () => ({
@@ -107,7 +109,9 @@ export function PostCtrls({
       <TouchableOpacity
         testID="replyBtn"
         style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
-        onPress={onPressReply}
+        onPress={() => {
+          requireAuth(() => onPressReply())
+        }}
         accessibilityRole="button"
         accessibilityLabel={`Reply (${post.replyCount} ${
           post.replyCount === 1 ? 'reply' : 'replies'
@@ -135,7 +139,9 @@ export function PostCtrls({
       <TouchableOpacity
         testID="likeBtn"
         style={[styles.ctrl, !big && styles.ctrlPad]}
-        onPress={onPressToggleLike}
+        onPress={() => {
+          requireAuth(() => onPressToggleLike())
+        }}
         accessibilityRole="button"
         accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${
           post.likeCount
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 0a7637252..1d34a88ab 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -7,6 +7,7 @@ import {Text} from '../text/Text'
 import {pluralize} from 'lib/strings/helpers'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
 import {useModalControls} from '#/state/modals'
+import {useRequireAuth} from '#/state/session'
 
 interface Props {
   isReposted: boolean
@@ -25,6 +26,7 @@ export const RepostButton = ({
 }: Props) => {
   const theme = useTheme()
   const {openModal} = useModalControls()
+  const requireAuth = useRequireAuth()
 
   const defaultControlColor = React.useMemo(
     () => ({
@@ -45,7 +47,9 @@ export const RepostButton = ({
   return (
     <TouchableOpacity
       testID="repostBtn"
-      onPress={onPressToggleRepostWrapper}
+      onPress={() => {
+        requireAuth(() => onPressToggleRepostWrapper())
+      }}
       style={[styles.control, !big && styles.controlPad]}
       accessibilityRole="button"
       accessibilityLabel={`${
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
index 6c5f816aa..329382132 100644
--- a/src/view/com/util/post-ctrls/RepostButton.web.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {StyleProp, StyleSheet, View, ViewStyle, Pressable} from 'react-native'
 import {RepostIcon} from 'lib/icons'
 import {colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
@@ -12,6 +12,8 @@ import {
 import {EventStopper} from '../EventStopper'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {useRequireAuth} from '#/state/session'
+import {useSession} from '#/state/session'
 
 interface Props {
   isReposted: boolean
@@ -31,6 +33,8 @@ export const RepostButton = ({
 }: Props) => {
   const theme = useTheme()
   const {_} = useLingui()
+  const {hasSession} = useSession()
+  const requireAuth = useRequireAuth()
 
   const defaultControlColor = React.useMemo(
     () => ({
@@ -62,32 +66,46 @@ export const RepostButton = ({
     },
   ]
 
-  return (
+  const inner = (
+    <View
+      style={[
+        styles.control,
+        !big && styles.controlPad,
+        (isReposted
+          ? styles.reposted
+          : defaultControlColor) as StyleProp<ViewStyle>,
+      ]}>
+      <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
+      {typeof repostCount !== 'undefined' ? (
+        <Text
+          testID="repostCount"
+          type={isReposted ? 'md-bold' : 'md'}
+          style={styles.repostCount}>
+          {repostCount ?? 0}
+        </Text>
+      ) : undefined}
+    </View>
+  )
+
+  return hasSession ? (
     <EventStopper>
       <NativeDropdown
         items={dropdownItems}
         accessibilityLabel={_(msg`Repost or quote post`)}
         accessibilityHint="">
-        <View
-          style={[
-            styles.control,
-            !big && styles.controlPad,
-            (isReposted
-              ? styles.reposted
-              : defaultControlColor) as StyleProp<ViewStyle>,
-          ]}>
-          <RepostIcon strokeWidth={2.2} size={big ? 24 : 20} />
-          {typeof repostCount !== 'undefined' ? (
-            <Text
-              testID="repostCount"
-              type={isReposted ? 'md-bold' : 'md'}
-              style={styles.repostCount}>
-              {repostCount ?? 0}
-            </Text>
-          ) : undefined}
-        </View>
+        {inner}
       </NativeDropdown>
     </EventStopper>
+  ) : (
+    <Pressable
+      accessibilityRole="button"
+      onPress={() => {
+        requireAuth(() => {})
+      }}
+      accessibilityLabel={_(msg`Repost or quote post`)}
+      accessibilityHint="">
+      {inner}
+    </Pressable>
   )
 }
 
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 5d62125ce..301c87d14 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -87,426 +87,429 @@ type FlatlistSlice =
       key: string
     }
 
-export const FeedsScreen = withAuthRequired(function FeedsScreenImpl(
-  _props: Props,
-) {
-  const pal = usePalette('default')
-  const {openComposer} = useComposerControls()
-  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
-  const [query, setQuery] = React.useState('')
-  const [isPTR, setIsPTR] = React.useState(false)
-  const {
-    data: preferences,
-    isLoading: isPreferencesLoading,
-    error: preferencesError,
-  } = usePreferencesQuery()
-  const {
-    data: popularFeeds,
-    isFetching: isPopularFeedsFetching,
-    error: popularFeedsError,
-    refetch: refetchPopularFeeds,
-    fetchNextPage: fetchNextPopularFeedsPage,
-    isFetchingNextPage: isPopularFeedsFetchingNextPage,
-    hasNextPage: hasNextPopularFeedsPage,
-  } = useGetPopularFeedsQuery()
-  const {_} = useLingui()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {
-    data: searchResults,
-    mutate: search,
-    reset: resetSearch,
-    isPending: isSearchPending,
-    error: searchError,
-  } = useSearchPopularFeedsMutation()
+export const FeedsScreen = withAuthRequired(
+  function FeedsScreenImpl(_props: Props) {
+    const pal = usePalette('default')
+    const {openComposer} = useComposerControls()
+    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+    const [query, setQuery] = React.useState('')
+    const [isPTR, setIsPTR] = React.useState(false)
+    const {
+      data: preferences,
+      isLoading: isPreferencesLoading,
+      error: preferencesError,
+    } = usePreferencesQuery()
+    const {
+      data: popularFeeds,
+      isFetching: isPopularFeedsFetching,
+      error: popularFeedsError,
+      refetch: refetchPopularFeeds,
+      fetchNextPage: fetchNextPopularFeedsPage,
+      isFetchingNextPage: isPopularFeedsFetchingNextPage,
+      hasNextPage: hasNextPopularFeedsPage,
+    } = useGetPopularFeedsQuery()
+    const {_} = useLingui()
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {
+      data: searchResults,
+      mutate: search,
+      reset: resetSearch,
+      isPending: isSearchPending,
+      error: searchError,
+    } = useSearchPopularFeedsMutation()
 
-  /**
-   * A search query is present. We may not have search results yet.
-   */
-  const isUserSearching = query.length > 1
-  const debouncedSearch = React.useMemo(
-    () => debounce(q => search(q), 500), // debounce for 500ms
-    [search],
-  )
-  const onPressCompose = React.useCallback(() => {
-    openComposer({})
-  }, [openComposer])
-  const onChangeQuery = React.useCallback(
-    (text: string) => {
-      setQuery(text)
-      if (text.length > 1) {
-        debouncedSearch(text)
-      } else {
-        refetchPopularFeeds()
-        resetSearch()
-      }
-    },
-    [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch],
-  )
-  const onPressCancelSearch = React.useCallback(() => {
-    setQuery('')
-    refetchPopularFeeds()
-    resetSearch()
-  }, [refetchPopularFeeds, setQuery, resetSearch])
-  const onSubmitQuery = React.useCallback(() => {
-    debouncedSearch(query)
-  }, [query, debouncedSearch])
-  const onPullToRefresh = React.useCallback(async () => {
-    setIsPTR(true)
-    await refetchPopularFeeds()
-    setIsPTR(false)
-  }, [setIsPTR, refetchPopularFeeds])
-  const onEndReached = React.useCallback(() => {
-    if (
-      isPopularFeedsFetching ||
-      isUserSearching ||
-      !hasNextPopularFeedsPage ||
-      popularFeedsError
+    /**
+     * A search query is present. We may not have search results yet.
+     */
+    const isUserSearching = query.length > 1
+    const debouncedSearch = React.useMemo(
+      () => debounce(q => search(q), 500), // debounce for 500ms
+      [search],
     )
-      return
-    fetchNextPopularFeedsPage()
-  }, [
-    isPopularFeedsFetching,
-    isUserSearching,
-    popularFeedsError,
-    hasNextPopularFeedsPage,
-    fetchNextPopularFeedsPage,
-  ])
-
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    const onPressCompose = React.useCallback(() => {
+      openComposer({})
+    }, [openComposer])
+    const onChangeQuery = React.useCallback(
+      (text: string) => {
+        setQuery(text)
+        if (text.length > 1) {
+          debouncedSearch(text)
+        } else {
+          refetchPopularFeeds()
+          resetSearch()
+        }
+      },
+      [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch],
+    )
+    const onPressCancelSearch = React.useCallback(() => {
+      setQuery('')
+      refetchPopularFeeds()
+      resetSearch()
+    }, [refetchPopularFeeds, setQuery, resetSearch])
+    const onSubmitQuery = React.useCallback(() => {
+      debouncedSearch(query)
+    }, [query, debouncedSearch])
+    const onPullToRefresh = React.useCallback(async () => {
+      setIsPTR(true)
+      await refetchPopularFeeds()
+      setIsPTR(false)
+    }, [setIsPTR, refetchPopularFeeds])
+    const onEndReached = React.useCallback(() => {
+      if (
+        isPopularFeedsFetching ||
+        isUserSearching ||
+        !hasNextPopularFeedsPage ||
+        popularFeedsError
+      )
+        return
+      fetchNextPopularFeedsPage()
+    }, [
+      isPopularFeedsFetching,
+      isUserSearching,
+      popularFeedsError,
+      hasNextPopularFeedsPage,
+      fetchNextPopularFeedsPage,
+    ])
 
-  const items = React.useMemo(() => {
-    let slices: FlatlistSlice[] = []
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-    slices.push({
-      key: 'savedFeedsHeader',
-      type: 'savedFeedsHeader',
-    })
+    const items = React.useMemo(() => {
+      let slices: FlatlistSlice[] = []
 
-    if (preferencesError) {
       slices.push({
-        key: 'savedFeedsError',
-        type: 'error',
-        error: cleanError(preferencesError.toString()),
+        key: 'savedFeedsHeader',
+        type: 'savedFeedsHeader',
       })
-    } else {
-      if (isPreferencesLoading || !preferences?.feeds?.saved) {
+
+      if (preferencesError) {
         slices.push({
-          key: 'savedFeedsLoading',
-          type: 'savedFeedsLoading',
-          // pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
+          key: 'savedFeedsError',
+          type: 'error',
+          error: cleanError(preferencesError.toString()),
         })
       } else {
-        if (preferences?.feeds?.saved.length === 0) {
+        if (isPreferencesLoading || !preferences?.feeds?.saved) {
           slices.push({
-            key: 'savedFeedNoResults',
-            type: 'savedFeedNoResults',
+            key: 'savedFeedsLoading',
+            type: 'savedFeedsLoading',
+            // pendingItems: this.rootStore.preferences.savedFeeds.length || 3,
           })
         } else {
-          const {saved, pinned} = preferences.feeds
-
-          slices = slices.concat(
-            pinned.map(uri => ({
-              key: `savedFeed:${uri}`,
-              type: 'savedFeed',
-              feedUri: uri,
-            })),
-          )
+          if (preferences?.feeds?.saved.length === 0) {
+            slices.push({
+              key: 'savedFeedNoResults',
+              type: 'savedFeedNoResults',
+            })
+          } else {
+            const {saved, pinned} = preferences.feeds
 
-          slices = slices.concat(
-            saved
-              .filter(uri => !pinned.includes(uri))
-              .map(uri => ({
+            slices = slices.concat(
+              pinned.map(uri => ({
                 key: `savedFeed:${uri}`,
                 type: 'savedFeed',
                 feedUri: uri,
               })),
-          )
+            )
+
+            slices = slices.concat(
+              saved
+                .filter(uri => !pinned.includes(uri))
+                .map(uri => ({
+                  key: `savedFeed:${uri}`,
+                  type: 'savedFeed',
+                  feedUri: uri,
+                })),
+            )
+          }
         }
       }
-    }
-
-    slices.push({
-      key: 'popularFeedsHeader',
-      type: 'popularFeedsHeader',
-    })
 
-    if (popularFeedsError || searchError) {
       slices.push({
-        key: 'popularFeedsError',
-        type: 'error',
-        error: cleanError(
-          popularFeedsError?.toString() ?? searchError?.toString() ?? '',
-        ),
+        key: 'popularFeedsHeader',
+        type: 'popularFeedsHeader',
       })
-    } else {
-      if (isUserSearching) {
-        if (isSearchPending || !searchResults) {
-          slices.push({
-            key: 'popularFeedsLoading',
-            type: 'popularFeedsLoading',
-          })
-        } else {
-          if (!searchResults || searchResults?.length === 0) {
+
+      if (popularFeedsError || searchError) {
+        slices.push({
+          key: 'popularFeedsError',
+          type: 'error',
+          error: cleanError(
+            popularFeedsError?.toString() ?? searchError?.toString() ?? '',
+          ),
+        })
+      } else {
+        if (isUserSearching) {
+          if (isSearchPending || !searchResults) {
             slices.push({
-              key: 'popularFeedsNoResults',
-              type: 'popularFeedsNoResults',
+              key: 'popularFeedsLoading',
+              type: 'popularFeedsLoading',
             })
           } else {
-            slices = slices.concat(
-              searchResults.map(feed => ({
-                key: `popularFeed:${feed.uri}`,
-                type: 'popularFeed',
-                feedUri: feed.uri,
-              })),
-            )
+            if (!searchResults || searchResults?.length === 0) {
+              slices.push({
+                key: 'popularFeedsNoResults',
+                type: 'popularFeedsNoResults',
+              })
+            } else {
+              slices = slices.concat(
+                searchResults.map(feed => ({
+                  key: `popularFeed:${feed.uri}`,
+                  type: 'popularFeed',
+                  feedUri: feed.uri,
+                })),
+              )
+            }
           }
-        }
-      } else {
-        if (isPopularFeedsFetching && !popularFeeds?.pages) {
-          slices.push({
-            key: 'popularFeedsLoading',
-            type: 'popularFeedsLoading',
-          })
         } else {
-          if (
-            !popularFeeds?.pages ||
-            popularFeeds?.pages[0]?.feeds?.length === 0
-          ) {
+          if (isPopularFeedsFetching && !popularFeeds?.pages) {
             slices.push({
-              key: 'popularFeedsNoResults',
-              type: 'popularFeedsNoResults',
+              key: 'popularFeedsLoading',
+              type: 'popularFeedsLoading',
             })
           } else {
-            for (const page of popularFeeds.pages || []) {
-              slices = slices.concat(
-                page.feeds
-                  .filter(feed => !preferences?.feeds?.saved.includes(feed.uri))
-                  .map(feed => ({
-                    key: `popularFeed:${feed.uri}`,
-                    type: 'popularFeed',
-                    feedUri: feed.uri,
-                  })),
-              )
-            }
-
-            if (isPopularFeedsFetchingNextPage) {
+            if (
+              !popularFeeds?.pages ||
+              popularFeeds?.pages[0]?.feeds?.length === 0
+            ) {
               slices.push({
-                key: 'popularFeedsLoadingMore',
-                type: 'popularFeedsLoadingMore',
+                key: 'popularFeedsNoResults',
+                type: 'popularFeedsNoResults',
               })
+            } else {
+              for (const page of popularFeeds.pages || []) {
+                slices = slices.concat(
+                  page.feeds
+                    .filter(
+                      feed => !preferences?.feeds?.saved.includes(feed.uri),
+                    )
+                    .map(feed => ({
+                      key: `popularFeed:${feed.uri}`,
+                      type: 'popularFeed',
+                      feedUri: feed.uri,
+                    })),
+                )
+              }
+
+              if (isPopularFeedsFetchingNextPage) {
+                slices.push({
+                  key: 'popularFeedsLoadingMore',
+                  type: 'popularFeedsLoadingMore',
+                })
+              }
             }
           }
         }
       }
-    }
 
-    return slices
-  }, [
-    preferences,
-    isPreferencesLoading,
-    preferencesError,
-    popularFeeds,
-    isPopularFeedsFetching,
-    popularFeedsError,
-    isPopularFeedsFetchingNextPage,
-    searchResults,
-    isSearchPending,
-    searchError,
-    isUserSearching,
-  ])
+      return slices
+    }, [
+      preferences,
+      isPreferencesLoading,
+      preferencesError,
+      popularFeeds,
+      isPopularFeedsFetching,
+      popularFeedsError,
+      isPopularFeedsFetchingNextPage,
+      searchResults,
+      isSearchPending,
+      searchError,
+      isUserSearching,
+    ])
 
-  const renderHeaderBtn = React.useCallback(() => {
-    return (
-      <Link
-        href="/settings/saved-feeds"
-        hitSlop={10}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`Edit Saved Feeds`)}
-        accessibilityHint="Opens screen to edit Saved Feeds">
-        <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
-      </Link>
-    )
-  }, [pal, _])
+    const renderHeaderBtn = React.useCallback(() => {
+      return (
+        <Link
+          href="/settings/saved-feeds"
+          hitSlop={10}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`Edit Saved Feeds`)}
+          accessibilityHint="Opens screen to edit Saved Feeds">
+          <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
+        </Link>
+      )
+    }, [pal, _])
 
-  const renderItem = React.useCallback(
-    ({item}: {item: FlatlistSlice}) => {
-      if (item.type === 'error') {
-        return <ErrorMessage message={item.error} />
-      } else if (
-        item.type === 'popularFeedsLoadingMore' ||
-        item.type === 'savedFeedsLoading'
-      ) {
-        return (
-          <View style={s.p10}>
-            <ActivityIndicator />
-          </View>
-        )
-      } else if (item.type === 'savedFeedsHeader') {
-        if (!isMobile) {
+    const renderItem = React.useCallback(
+      ({item}: {item: FlatlistSlice}) => {
+        if (item.type === 'error') {
+          return <ErrorMessage message={item.error} />
+        } else if (
+          item.type === 'popularFeedsLoadingMore' ||
+          item.type === 'savedFeedsLoading'
+        ) {
           return (
-            <View
-              style={[
-                pal.view,
-                styles.header,
-                pal.border,
-                {
-                  borderBottomWidth: 1,
-                },
-              ]}>
-              <Text type="title-lg" style={[pal.text, s.bold]}>
-                <Trans>My Feeds</Trans>
-              </Text>
-              <Link
-                href="/settings/saved-feeds"
-                accessibilityLabel={_(msg`Edit My Feeds`)}
-                accessibilityHint="">
-                <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
-              </Link>
+            <View style={s.p10}>
+              <ActivityIndicator />
             </View>
           )
-        }
-        return <View />
-      } else if (item.type === 'savedFeedNoResults') {
-        return (
-          <View
-            style={{
-              paddingHorizontal: 16,
-              paddingTop: 10,
-            }}>
-            <Text type="lg" style={pal.textLight}>
-              <Trans>You don't have any saved feeds!</Trans>
-            </Text>
-          </View>
-        )
-      } else if (item.type === 'savedFeed') {
-        return <SavedFeed feedUri={item.feedUri} />
-      } else if (item.type === 'popularFeedsHeader') {
-        return (
-          <>
+        } else if (item.type === 'savedFeedsHeader') {
+          if (!isMobile) {
+            return (
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  pal.border,
+                  {
+                    borderBottomWidth: 1,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  <Trans>My Feeds</Trans>
+                </Text>
+                <Link
+                  href="/settings/saved-feeds"
+                  accessibilityLabel={_(msg`Edit My Feeds`)}
+                  accessibilityHint="">
+                  <CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
+                </Link>
+              </View>
+            )
+          }
+          return <View />
+        } else if (item.type === 'savedFeedNoResults') {
+          return (
             <View
-              style={[
-                pal.view,
-                styles.header,
-                {
-                  marginTop: 16,
-                  paddingLeft: isMobile ? 12 : undefined,
-                  paddingRight: 10,
-                  paddingBottom: isMobile ? 6 : undefined,
-                },
-              ]}>
-              <Text type="title-lg" style={[pal.text, s.bold]}>
-                <Trans>Discover new feeds</Trans>
+              style={{
+                paddingHorizontal: 16,
+                paddingTop: 10,
+              }}>
+              <Text type="lg" style={pal.textLight}>
+                <Trans>You don't have any saved feeds!</Trans>
               </Text>
+            </View>
+          )
+        } else if (item.type === 'savedFeed') {
+          return <SavedFeed feedUri={item.feedUri} />
+        } else if (item.type === 'popularFeedsHeader') {
+          return (
+            <>
+              <View
+                style={[
+                  pal.view,
+                  styles.header,
+                  {
+                    marginTop: 16,
+                    paddingLeft: isMobile ? 12 : undefined,
+                    paddingRight: 10,
+                    paddingBottom: isMobile ? 6 : undefined,
+                  },
+                ]}>
+                <Text type="title-lg" style={[pal.text, s.bold]}>
+                  <Trans>Discover new feeds</Trans>
+                </Text>
+
+                {!isMobile && (
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                    style={{flex: 1, maxWidth: 250}}
+                  />
+                )}
+              </View>
 
-              {!isMobile && (
-                <SearchInput
-                  query={query}
-                  onChangeQuery={onChangeQuery}
-                  onPressCancelSearch={onPressCancelSearch}
-                  onSubmitQuery={onSubmitQuery}
-                  style={{flex: 1, maxWidth: 250}}
-                />
+              {isMobile && (
+                <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
+                  <SearchInput
+                    query={query}
+                    onChangeQuery={onChangeQuery}
+                    onPressCancelSearch={onPressCancelSearch}
+                    onSubmitQuery={onSubmitQuery}
+                  />
+                </View>
               )}
+            </>
+          )
+        } else if (item.type === 'popularFeedsLoading') {
+          return <FeedFeedLoadingPlaceholder />
+        } else if (item.type === 'popularFeed') {
+          return (
+            <FeedSourceCard
+              feedUri={item.feedUri}
+              showSaveBtn
+              showDescription
+              showLikes
+            />
+          )
+        } else if (item.type === 'popularFeedsNoResults') {
+          return (
+            <View
+              style={{
+                paddingHorizontal: 16,
+                paddingTop: 10,
+                paddingBottom: '150%',
+              }}>
+              <Text type="lg" style={pal.textLight}>
+                <Trans>No results found for "{query}"</Trans>
+              </Text>
             </View>
+          )
+        }
+        return null
+      },
+      [
+        _,
+        isMobile,
+        pal,
+        query,
+        onChangeQuery,
+        onPressCancelSearch,
+        onSubmitQuery,
+      ],
+    )
 
-            {isMobile && (
-              <View style={{paddingHorizontal: 8, paddingBottom: 10}}>
-                <SearchInput
-                  query={query}
-                  onChangeQuery={onChangeQuery}
-                  onPressCancelSearch={onPressCancelSearch}
-                  onSubmitQuery={onSubmitQuery}
-                />
-              </View>
-            )}
-          </>
-        )
-      } else if (item.type === 'popularFeedsLoading') {
-        return <FeedFeedLoadingPlaceholder />
-      } else if (item.type === 'popularFeed') {
-        return (
-          <FeedSourceCard
-            feedUri={item.feedUri}
-            showSaveBtn
-            showDescription
-            showLikes
+    return (
+      <View style={[pal.view, styles.container]}>
+        {isMobile && (
+          <ViewHeader
+            title={_(msg`Feeds`)}
+            canGoBack={false}
+            renderButton={renderHeaderBtn}
+            showBorder
           />
-        )
-      } else if (item.type === 'popularFeedsNoResults') {
-        return (
-          <View
-            style={{
-              paddingHorizontal: 16,
-              paddingTop: 10,
-              paddingBottom: '150%',
-            }}>
-            <Text type="lg" style={pal.textLight}>
-              <Trans>No results found for "{query}"</Trans>
-            </Text>
-          </View>
-        )
-      }
-      return null
-    },
-    [
-      _,
-      isMobile,
-      pal,
-      query,
-      onChangeQuery,
-      onPressCancelSearch,
-      onSubmitQuery,
-    ],
-  )
+        )}
 
-  return (
-    <View style={[pal.view, styles.container]}>
-      {isMobile && (
-        <ViewHeader
-          title={_(msg`Feeds`)}
-          canGoBack={false}
-          renderButton={renderHeaderBtn}
-          showBorder
-        />
-      )}
-
-      {preferences ? <View /> : <ActivityIndicator />}
+        {preferences ? <View /> : <ActivityIndicator />}
 
-      <FlatList
-        style={[!isTabletOrDesktop && s.flex1, styles.list]}
-        data={items}
-        keyExtractor={item => item.key}
-        contentContainerStyle={styles.contentContainer}
-        renderItem={renderItem}
-        refreshControl={
-          <RefreshControl
-            refreshing={isPTR}
-            onRefresh={isUserSearching ? undefined : onPullToRefresh}
-            tintColor={pal.colors.text}
-            titleColor={pal.colors.text}
-          />
-        }
-        initialNumToRender={10}
-        onEndReached={onEndReached}
-        // @ts-ignore our .web version only -prf
-        desktopFixedHeight
-      />
+        <FlatList
+          style={[!isTabletOrDesktop && s.flex1, styles.list]}
+          data={items}
+          keyExtractor={item => item.key}
+          contentContainerStyle={styles.contentContainer}
+          renderItem={renderItem}
+          refreshControl={
+            <RefreshControl
+              refreshing={isPTR}
+              onRefresh={isUserSearching ? undefined : onPullToRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+            />
+          }
+          initialNumToRender={10}
+          onEndReached={onEndReached}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+        />
 
-      <FAB
-        testID="composeFAB"
-        onPress={onPressCompose}
-        icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
-        accessibilityRole="button"
-        accessibilityLabel={_(msg`New post`)}
-        accessibilityHint=""
-      />
-    </View>
-  )
-})
+        <FAB
+          testID="composeFAB"
+          onPress={onPressCompose}
+          icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
+          accessibilityRole="button"
+          accessibilityLabel={_(msg`New post`)}
+          accessibilityHint=""
+        />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
 
 function SavedFeed({feedUri}: {feedUri: string}) {
   const pal = usePalette('default')
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 18a0cbc15..8df945cd2 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -14,22 +14,56 @@ import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
 import {emitSoftReset} from '#/state/events'
+import {useSession} from '#/state/session'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
-export const HomeScreen = withAuthRequired(function HomeScreenImpl(
-  props: Props,
-) {
-  const {data: preferences} = usePreferencesQuery()
-  if (preferences) {
-    return <HomeScreenReady {...props} preferences={preferences} />
-  } else {
-    return (
-      <View style={styles.loading}>
-        <ActivityIndicator size="large" />
-      </View>
-    )
-  }
-})
+export const HomeScreen = withAuthRequired(
+  function HomeScreenImpl(props: Props) {
+    const {hasSession} = useSession()
+    const {data: preferences} = usePreferencesQuery()
+
+    if (!hasSession) {
+      return <HomeScreenPublic />
+    }
+
+    if (preferences) {
+      return <HomeScreenReady {...props} preferences={preferences} />
+    } else {
+      return (
+        <View style={styles.loading}>
+          <ActivityIndicator size="large" />
+        </View>
+      )
+    }
+  },
+  {
+    isPublic: true,
+  },
+)
+
+function HomeScreenPublic() {
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+
+  const renderCustomFeedEmptyState = React.useCallback(() => {
+    return <CustomFeedEmptyState />
+  }, [])
+
+  useFocusEffect(
+    React.useCallback(() => {
+      setMinimalShellMode(false)
+      setDrawerSwipeDisabled(false)
+    }, [setDrawerSwipeDisabled, setMinimalShellMode]),
+  )
+
+  return (
+    <FeedPage
+      isPageFocused
+      feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`}
+      renderEmptyState={renderCustomFeedEmptyState}
+    />
+  )
+}
 
 function HomeScreenReady({
   preferences,
@@ -83,6 +117,7 @@ function HomeScreenReady({
     emitSoftReset()
   }, [])
 
+  // TODO(pwi) may need this in public view
   const onPageScrollStateChanged = React.useCallback(
     (state: 'idle' | 'dragging' | 'settling') => {
       if (state === 'dragging') {
diff --git a/src/view/screens/PostLikedBy.tsx b/src/view/screens/PostLikedBy.tsx
index ab7bbcefe..2209310d0 100644
--- a/src/view/screens/PostLikedBy.tsx
+++ b/src/view/screens/PostLikedBy.tsx
@@ -11,22 +11,25 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostLikedBy'>
-export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {name, rkey} = route.params
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const {_} = useLingui()
+export const PostLikedByScreen = withAuthRequired(
+  ({route}: Props) => {
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {name, rkey} = route.params
+    const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
+    const {_} = useLingui()
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-  return (
-    <View>
-      <ViewHeader title={_(msg`Liked by`)} />
-      <PostLikedByComponent uri={uri} />
-    </View>
-  )
-})
+    return (
+      <View>
+        <ViewHeader title={_(msg`Liked by`)} />
+        <PostLikedByComponent uri={uri} />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
diff --git a/src/view/screens/PostRepostedBy.tsx b/src/view/screens/PostRepostedBy.tsx
index eabbc4a4e..5b3b5f8fa 100644
--- a/src/view/screens/PostRepostedBy.tsx
+++ b/src/view/screens/PostRepostedBy.tsx
@@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostRepostedBy'>
-export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
-  const {name, rkey} = route.params
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {_} = useLingui()
+export const PostRepostedByScreen = withAuthRequired(
+  ({route}: Props) => {
+    const {name, rkey} = route.params
+    const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {_} = useLingui()
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-  return (
-    <View>
-      <ViewHeader title={_(msg`Reposted by`)} />
-      <PostRepostedByComponent uri={uri} />
-    </View>
-  )
-})
+    return (
+      <View>
+        <ViewHeader title={_(msg`Reposted by`)} />
+        <PostRepostedByComponent uri={uri} />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 0476e182b..11574e283 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -27,84 +27,85 @@ import {CenteredView} from '../com/util/Views'
 import {useComposerControls} from '#/state/shell/composer'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostThread'>
-export const PostThreadScreen = withAuthRequired(function PostThreadScreenImpl({
-  route,
-}: Props) {
-  const queryClient = useQueryClient()
-  const {_} = useLingui()
-  const {fabMinimalShellTransform} = useMinimalShellMode()
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {openComposer} = useComposerControls()
-  const safeAreaInsets = useSafeAreaInsets()
-  const {name, rkey} = route.params
-  const {isMobile} = useWebMediaQueries()
-  const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
+export const PostThreadScreen = withAuthRequired(
+  function PostThreadScreenImpl({route}: Props) {
+    const queryClient = useQueryClient()
+    const {_} = useLingui()
+    const {fabMinimalShellTransform} = useMinimalShellMode()
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {openComposer} = useComposerControls()
+    const safeAreaInsets = useSafeAreaInsets()
+    const {name, rkey} = route.params
+    const {isMobile} = useWebMediaQueries()
+    const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
+    const {data: resolvedUri, error: uriError} = useResolveUriQuery(uri)
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
-
-  const onPressReply = React.useCallback(() => {
-    if (!resolvedUri) {
-      return
-    }
-    const thread = queryClient.getQueryData<ThreadNode>(
-      POST_THREAD_RQKEY(resolvedUri.uri),
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
     )
-    if (thread?.type !== 'post') {
-      return
-    }
-    openComposer({
-      replyTo: {
-        uri: thread.post.uri,
-        cid: thread.post.cid,
-        text: thread.record.text,
-        author: {
-          handle: thread.post.author.handle,
-          displayName: thread.post.author.displayName,
-          avatar: thread.post.author.avatar,
+
+    const onPressReply = React.useCallback(() => {
+      if (!resolvedUri) {
+        return
+      }
+      const thread = queryClient.getQueryData<ThreadNode>(
+        POST_THREAD_RQKEY(resolvedUri.uri),
+      )
+      if (thread?.type !== 'post') {
+        return
+      }
+      openComposer({
+        replyTo: {
+          uri: thread.post.uri,
+          cid: thread.post.cid,
+          text: thread.record.text,
+          author: {
+            handle: thread.post.author.handle,
+            displayName: thread.post.author.displayName,
+            avatar: thread.post.author.avatar,
+          },
         },
-      },
-      onPost: () =>
-        queryClient.invalidateQueries({
-          queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
-        }),
-    })
-  }, [openComposer, queryClient, resolvedUri])
+        onPost: () =>
+          queryClient.invalidateQueries({
+            queryKey: POST_THREAD_RQKEY(resolvedUri.uri || ''),
+          }),
+      })
+    }, [openComposer, queryClient, resolvedUri])
 
-  return (
-    <View style={s.hContentRegion}>
-      {isMobile && <ViewHeader title={_(msg`Post`)} />}
-      <View style={s.flex1}>
-        {uriError ? (
-          <CenteredView>
-            <ErrorMessage message={String(uriError)} />
-          </CenteredView>
-        ) : (
-          <PostThreadComponent
-            uri={resolvedUri?.uri}
-            onPressReply={onPressReply}
-          />
+    return (
+      <View style={s.hContentRegion}>
+        {isMobile && <ViewHeader title={_(msg`Post`)} />}
+        <View style={s.flex1}>
+          {uriError ? (
+            <CenteredView>
+              <ErrorMessage message={String(uriError)} />
+            </CenteredView>
+          ) : (
+            <PostThreadComponent
+              uri={resolvedUri?.uri}
+              onPressReply={onPressReply}
+            />
+          )}
+        </View>
+        {isMobile && (
+          <Animated.View
+            style={[
+              styles.prompt,
+              fabMinimalShellTransform,
+              {
+                bottom: clamp(safeAreaInsets.bottom, 15, 30),
+              },
+            ]}>
+            <ComposePrompt onPressCompose={onPressReply} />
+          </Animated.View>
         )}
       </View>
-      {isMobile && (
-        <Animated.View
-          style={[
-            styles.prompt,
-            fabMinimalShellTransform,
-            {
-              bottom: clamp(safeAreaInsets.bottom, 15, 30),
-            },
-          ]}>
-          <ComposePrompt onPressCompose={onPressReply} />
-        </Animated.View>
-      )}
-    </View>
-  )
-})
+    )
+  },
+  {isPublic: true},
+)
 
 const styles = StyleSheet.create({
   prompt: {
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 88b11b114..5411bc044 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -43,82 +43,85 @@ interface SectionRef {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
-export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
-  route,
-}: Props) {
-  const {currentAccount} = useSession()
-  const name =
-    route.params.name === 'me' ? currentAccount?.did : route.params.name
-  const moderationOpts = useModerationOpts()
-  const {
-    data: resolvedDid,
-    error: resolveError,
-    refetch: refetchDid,
-    isFetching: isFetchingDid,
-  } = useResolveDidQuery(name)
-  const {
-    data: profile,
-    dataUpdatedAt,
-    error: profileError,
-    refetch: refetchProfile,
-    isFetching: isFetchingProfile,
-  } = useProfileQuery({
-    did: resolvedDid?.did,
-  })
+export const ProfileScreen = withAuthRequired(
+  function ProfileScreenImpl({route}: Props) {
+    const {currentAccount} = useSession()
+    const name =
+      route.params.name === 'me' ? currentAccount?.did : route.params.name
+    const moderationOpts = useModerationOpts()
+    const {
+      data: resolvedDid,
+      error: resolveError,
+      refetch: refetchDid,
+      isFetching: isFetchingDid,
+    } = useResolveDidQuery(name)
+    const {
+      data: profile,
+      dataUpdatedAt,
+      error: profileError,
+      refetch: refetchProfile,
+      isFetching: isFetchingProfile,
+    } = useProfileQuery({
+      did: resolvedDid?.did,
+    })
 
-  const onPressTryAgain = React.useCallback(() => {
-    if (resolveError) {
-      refetchDid()
-    } else {
-      refetchProfile()
-    }
-  }, [resolveError, refetchDid, refetchProfile])
+    const onPressTryAgain = React.useCallback(() => {
+      if (resolveError) {
+        refetchDid()
+      } else {
+        refetchProfile()
+      }
+    }, [resolveError, refetchDid, refetchProfile])
 
-  if (isFetchingDid || isFetchingProfile || !moderationOpts) {
-    return (
-      <CenteredView>
-        <ProfileHeader
-          profile={null}
-          moderation={null}
-          isProfilePreview={true}
+    if (isFetchingDid || isFetchingProfile || !moderationOpts) {
+      return (
+        <CenteredView>
+          <ProfileHeader
+            profile={null}
+            moderation={null}
+            isProfilePreview={true}
+          />
+        </CenteredView>
+      )
+    }
+    if (resolveError || profileError) {
+      return (
+        <CenteredView>
+          <ErrorScreen
+            testID="profileErrorScreen"
+            title="Oops!"
+            message={cleanError(resolveError || profileError)}
+            onPressTryAgain={onPressTryAgain}
+          />
+        </CenteredView>
+      )
+    }
+    if (profile && moderationOpts) {
+      return (
+        <ProfileScreenLoaded
+          profile={profile}
+          dataUpdatedAt={dataUpdatedAt}
+          moderationOpts={moderationOpts}
+          hideBackButton={!!route.params.hideBackButton}
         />
-      </CenteredView>
-    )
-  }
-  if (resolveError || profileError) {
+      )
+    }
+    // should never happen
     return (
       <CenteredView>
         <ErrorScreen
           testID="profileErrorScreen"
           title="Oops!"
-          message={cleanError(resolveError || profileError)}
+          message="Something went wrong and we're not sure what."
           onPressTryAgain={onPressTryAgain}
         />
       </CenteredView>
     )
-  }
-  if (profile && moderationOpts) {
-    return (
-      <ProfileScreenLoaded
-        profile={profile}
-        dataUpdatedAt={dataUpdatedAt}
-        moderationOpts={moderationOpts}
-        hideBackButton={!!route.params.hideBackButton}
-      />
-    )
-  }
-  // should never happen
-  return (
-    <CenteredView>
-      <ErrorScreen
-        testID="profileErrorScreen"
-        title="Oops!"
-        message="Something went wrong and we're not sure what."
-        onPressTryAgain={onPressTryAgain}
-      />
-    </CenteredView>
-  )
-})
+  },
+  {
+    isPublic: true,
+  },
+)
 
 function ProfileScreenLoaded({
   profile: profileUnshadowed,
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 3974d3a11..2cdcdb3b5 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -129,6 +129,9 @@ export const ProfileFeedScreen = withAuthRequired(
       </CenteredView>
     )
   },
+  {
+    isPublic: true,
+  },
 )
 
 function ProfileFeedScreenIntermediate({feedUri}: {feedUri: string}) {
diff --git a/src/view/screens/ProfileFeedLikedBy.tsx b/src/view/screens/ProfileFeedLikedBy.tsx
index c8466360e..6399c8a0b 100644
--- a/src/view/screens/ProfileFeedLikedBy.tsx
+++ b/src/view/screens/ProfileFeedLikedBy.tsx
@@ -11,22 +11,25 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeedLikedBy'>
-export const ProfileFeedLikedByScreen = withAuthRequired(({route}: Props) => {
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {name, rkey} = route.params
-  const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
-  const {_} = useLingui()
+export const ProfileFeedLikedByScreen = withAuthRequired(
+  ({route}: Props) => {
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {name, rkey} = route.params
+    const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey)
+    const {_} = useLingui()
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-  return (
-    <View>
-      <ViewHeader title={_(msg`Liked by`)} />
-      <PostLikedByComponent uri={uri} />
-    </View>
-  )
-})
+    return (
+      <View>
+        <ViewHeader title={_(msg`Liked by`)} />
+        <PostLikedByComponent uri={uri} />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
diff --git a/src/view/screens/ProfileFollowers.tsx b/src/view/screens/ProfileFollowers.tsx
index 13e69541a..71c0e4a9c 100644
--- a/src/view/screens/ProfileFollowers.tsx
+++ b/src/view/screens/ProfileFollowers.tsx
@@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollowers'>
-export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
-  const {name} = route.params
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {_} = useLingui()
+export const ProfileFollowersScreen = withAuthRequired(
+  ({route}: Props) => {
+    const {name} = route.params
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {_} = useLingui()
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-  return (
-    <View>
-      <ViewHeader title={_(msg`Followers`)} />
-      <ProfileFollowersComponent name={name} />
-    </View>
-  )
-})
+    return (
+      <View>
+        <ViewHeader title={_(msg`Followers`)} />
+        <ProfileFollowersComponent name={name} />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
diff --git a/src/view/screens/ProfileFollows.tsx b/src/view/screens/ProfileFollows.tsx
index 07d6eaa78..bb3f2040f 100644
--- a/src/view/screens/ProfileFollows.tsx
+++ b/src/view/screens/ProfileFollows.tsx
@@ -10,21 +10,24 @@ import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFollows'>
-export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
-  const {name} = route.params
-  const setMinimalShellMode = useSetMinimalShellMode()
-  const {_} = useLingui()
+export const ProfileFollowsScreen = withAuthRequired(
+  ({route}: Props) => {
+    const {name} = route.params
+    const setMinimalShellMode = useSetMinimalShellMode()
+    const {_} = useLingui()
 
-  useFocusEffect(
-    React.useCallback(() => {
-      setMinimalShellMode(false)
-    }, [setMinimalShellMode]),
-  )
+    useFocusEffect(
+      React.useCallback(() => {
+        setMinimalShellMode(false)
+      }, [setMinimalShellMode]),
+    )
 
-  return (
-    <View>
-      <ViewHeader title={_(msg`Following`)} />
-      <ProfileFollowsComponent name={name} />
-    </View>
-  )
-})
+    return (
+      <View>
+        <ViewHeader title={_(msg`Following`)} />
+        <ProfileFollowsComponent name={name} />
+      </View>
+    )
+  },
+  {isPublic: true},
+)
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 4928b6745..4c13a2be1 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -33,7 +33,7 @@ import {
   usePinFeedMutation,
   useUnpinFeedMutation,
   useSetSaveFeedsMutation,
-  usePreferencesQueryKey,
+  preferencesQueryKey,
   UsePreferencesQueryResponse,
 } from '#/state/queries/preferences'
 
@@ -182,9 +182,10 @@ function ListItem({
   const onPressUp = React.useCallback(async () => {
     if (!isPinned) return
 
-    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
-      usePreferencesQueryKey,
-    )?.feeds
+    const feeds =
+      queryClient.getQueryData<UsePreferencesQueryResponse>(
+        preferencesQueryKey,
+      )?.feeds
     const pinned = feeds?.pinned ?? []
     const index = pinned.indexOf(feedUri)
 
@@ -206,9 +207,10 @@ function ListItem({
   const onPressDown = React.useCallback(async () => {
     if (!isPinned) return
 
-    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
-      usePreferencesQueryKey,
-    )?.feeds
+    const feeds =
+      queryClient.getQueryData<UsePreferencesQueryResponse>(
+        preferencesQueryKey,
+      )?.feeds
     const pinned = feeds?.pinned ?? []
     const index = pinned.indexOf(feedUri)