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/composer/Composer.tsx2
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx10
-rw-r--r--src/view/com/feeds/FeedPage.tsx2
-rw-r--r--src/view/com/pager/Pager.tsx2
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx15
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx31
-rw-r--r--src/view/com/post/Post.tsx8
-rw-r--r--src/view/com/posts/Feed.tsx37
-rw-r--r--src/view/com/posts/FeedErrorMessage.tsx11
-rw-r--r--src/view/com/posts/FeedItem.tsx8
-rw-r--r--src/view/com/posts/FollowingEndOfFeed.tsx9
-rw-r--r--src/view/com/profile/ProfileCard.tsx3
-rw-r--r--src/view/com/profile/ProfileHeader.tsx32
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx8
-rw-r--r--src/view/com/util/LoadingPlaceholder.tsx55
-rw-r--r--src/view/com/util/moderation/ContentHider.tsx43
-rw-r--r--src/view/com/util/moderation/PostHider.tsx126
-rw-r--r--src/view/com/util/moderation/ProfileHeaderAlerts.tsx16
-rw-r--r--src/view/com/util/post-embeds/index.tsx15
-rw-r--r--src/view/screens/Home.tsx9
-rw-r--r--src/view/screens/Profile.tsx19
-rw-r--r--src/view/screens/ProfileFeed.tsx7
-rw-r--r--src/view/screens/ProfileList.tsx55
-rw-r--r--src/view/shell/createNativeStackNavigatorWithAuth.tsx3
24 files changed, 366 insertions, 160 deletions
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 6f058d39e..7336f3b95 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -201,7 +201,7 @@ export const ComposePost = observer(function ComposePost({
 
     setError('')
 
-    if (richtext.text.trim().length === 0 && gallery.isEmpty) {
+    if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) {
       setError('Did you want to say anything?')
       return
     }
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 4c31da338..206a3205b 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -116,6 +116,16 @@ export const TextInput = React.forwardRef(function TextInputImpl(
       autofocus: 'end',
       editable: true,
       injectCSS: true,
+      onCreate({editor: editorProp}) {
+        // HACK
+        // the 'enter' animation sometimes causes autofocus to fail
+        // (see Composer.web.tsx in shell)
+        // so we wait 200ms (the anim is 150ms) and then focus manually
+        // -prf
+        setTimeout(() => {
+          editorProp.chain().focus('end').run()
+        }, 200)
+      },
       onUpdate({editor: editorProp}) {
         const json = editorProp.getJSON()
 
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index f06716fb0..f3f07a8bd 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -158,9 +158,9 @@ export function FeedPage({
     <View testID={testID} style={s.h100pct}>
       <Feed
         testID={testID ? `${testID}-feed` : undefined}
+        enabled={isPageFocused}
         feed={feed}
         feedParams={feedParams}
-        enabled={isPageFocused}
         pollInterval={POLL_FREQ}
         scrollElRef={scrollElRef}
         onScroll={onMainScroll}
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index d70087504..61c3609f2 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -81,12 +81,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
         if (scrollState.current === 'settling') {
           if (lastDirection.current === -1 && offset < lastOffset.current) {
             onPageSelecting?.(position)
+            setSelectedPage(position)
             lastDirection.current = 0
           } else if (
             lastDirection.current === 1 &&
             offset > lastOffset.current
           ) {
             onPageSelecting?.(position + 1)
+            setSelectedPage(position + 1)
             lastDirection.current = 0
           }
         } else {
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 487c589e3..dcfc1eebb 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -69,13 +69,19 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
     // capture the header bar sizing
     const onTabBarLayout = React.useCallback(
       (evt: LayoutChangeEvent) => {
-        setTabBarHeight(evt.nativeEvent.layout.height)
+        const height = evt.nativeEvent.layout.height
+        if (height > 0) {
+          setTabBarHeight(height)
+        }
       },
       [setTabBarHeight],
     )
     const onHeaderOnlyLayout = React.useCallback(
       (evt: LayoutChangeEvent) => {
-        setHeaderOnlyHeight(evt.nativeEvent.layout.height)
+        const height = evt.nativeEvent.layout.height
+        if (height > 0) {
+          setHeaderOnlyHeight(height)
+        }
       },
       [setHeaderOnlyHeight],
     )
@@ -248,11 +254,14 @@ let PagerTabBar = ({
   }))
   return (
     <Animated.View
+      pointerEvents="box-none"
       style={[
         isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
         headerTransform,
       ]}>
-      <View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View>
+      <View onLayout={onHeaderOnlyLayout} pointerEvents="box-none">
+        {renderHeader?.()}
+      </View>
       <View
         onLayout={onTabBarLayout}
         style={{
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index a4b7a4a9c..86ea4eb39 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -351,11 +351,14 @@ let PostThreadItemLoaded = ({
               {post.embed && (
                 <ContentHider
                   moderation={moderation.embed}
+                  moderationDecisions={moderation.decisions}
                   ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
+                  ignoreQuoteDecisions
                   style={s.mb10}>
                   <PostEmbeds
                     embed={post.embed}
                     moderation={moderation.embed}
+                    moderationDecisions={moderation.decisions}
                   />
                 </ContentHider>
               )}
@@ -414,7 +417,7 @@ let PostThreadItemLoaded = ({
       </>
     )
   } else {
-    const isThreadedChild = treeView && depth > 1
+    const isThreadedChild = treeView && depth > 0
     return (
       <PostOuterWrapper
         post={post}
@@ -426,7 +429,11 @@ let PostThreadItemLoaded = ({
           testID={`postThreadItem-by-${post.author.handle}`}
           href={postHref}
           style={[pal.view]}
-          moderation={moderation.content}>
+          moderation={moderation.content}
+          iconSize={isThreadedChild ? 26 : 38}
+          iconStyles={
+            isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
+          }>
           <PostSandboxWarning />
 
           <View
@@ -491,10 +498,10 @@ let PostThreadItemLoaded = ({
                 timestamp={post.indexedAt}
                 postHref={postHref}
                 showAvatar={isThreadedChild}
-                avatarSize={26}
+                avatarSize={20}
                 displayNameType="md-bold"
                 displayNameStyle={isThreadedChild && s.ml2}
-                style={isThreadedChild && s.mb5}
+                style={isThreadedChild && s.mb2}
               />
               <PostAlerts
                 moderation={moderation.content}
@@ -522,10 +529,14 @@ let PostThreadItemLoaded = ({
               {post.embed && (
                 <ContentHider
                   style={styles.contentHider}
-                  moderation={moderation.embed}>
+                  moderation={moderation.embed}
+                  moderationDecisions={moderation.decisions}
+                  ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
+                  ignoreQuoteDecisions>
                   <PostEmbeds
                     embed={post.embed}
                     moderation={moderation.embed}
+                    moderationDecisions={moderation.decisions}
                   />
                 </ContentHider>
               )}
@@ -583,7 +594,7 @@ function PostOuterWrapper({
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
   const styles = useStyles()
-  if (treeView && depth > 1) {
+  if (treeView && depth > 0) {
     return (
       <View
         style={[
@@ -592,9 +603,9 @@ function PostOuterWrapper({
           styles.cursor,
           {
             flexDirection: 'row',
-            paddingLeft: 20,
+            paddingLeft: depth === 1 ? 10 : 20,
             borderTopWidth: depth === 1 ? 1 : 0,
-            paddingTop: depth === 1 ? 8 : 0,
+            paddingTop: depth === 1 ? 6 : 0,
           },
         ]}>
         {Array.from(Array(depth - 1)).map((_, n: number) => (
@@ -603,8 +614,8 @@ function PostOuterWrapper({
             style={{
               borderLeftWidth: 2,
               borderLeftColor: pal.colors.border,
-              marginLeft: n === 0 ? 14 : isMobile ? 6 : 14,
-              paddingLeft: n === 0 ? 18 : isMobile ? 6 : 12,
+              marginLeft: isMobile ? 6 : 14,
+              paddingLeft: isMobile ? 6 : 12,
             }}
           />
         ))}
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 2e8019e71..9b1bf7a49 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -196,8 +196,14 @@ function PostInner({
             {post.embed ? (
               <ContentHider
                 moderation={moderation.embed}
+                moderationDecisions={moderation.decisions}
+                ignoreQuoteDecisions
                 style={styles.contentHider}>
-                <PostEmbeds embed={post.embed} moderation={moderation.embed} />
+                <PostEmbeds
+                  embed={post.embed}
+                  moderation={moderation.embed}
+                  moderationDecisions={moderation.decisions}
+                />
               </ContentHider>
             ) : null}
           </ContentHider>
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 393c1bc91..b9ca9abdc 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -26,6 +26,7 @@ import {
   pollLatest,
 } from '#/state/queries/post-feed'
 import {useModerationOpts} from '#/state/queries/preferences'
+import {isWeb} from '#/platform/detection'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -89,7 +90,7 @@ let Feed = ({
   const isEmpty = !isFetching && !data?.pages[0]?.slices.length
 
   const checkForNew = React.useCallback(async () => {
-    if (!data?.pages[0] || isFetching || !onHasNew) {
+    if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
       return
     }
     try {
@@ -99,7 +100,7 @@ let Feed = ({
     } catch (e) {
       logger.error('Poll latest failed', {feed, error: String(e)})
     }
-  }, [feed, data, isFetching, onHasNew])
+  }, [feed, data, isFetching, onHasNew, enabled])
 
   React.useEffect(() => {
     // we store the interval handler in a ref to avoid needless
@@ -216,19 +217,25 @@ let Feed = ({
 
   const shouldRenderEndOfFeed =
     !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed
-  const FeedFooter = React.useCallback(
-    () =>
-      isFetchingNextPage ? (
-        <View style={styles.feedFooter}>
-          <ActivityIndicator />
-        </View>
-      ) : shouldRenderEndOfFeed ? (
-        renderEndOfFeed()
-      ) : (
-        <View />
-      ),
-    [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed],
-  )
+  const FeedFooter = React.useCallback(() => {
+    /**
+     * A bit of padding at the bottom of the feed as you scroll and when you
+     * reach the end, so that content isn't cut off by the bottom of the
+     * screen.
+     */
+    const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2)
+
+    return isFetchingNextPage ? (
+      <View style={[styles.feedFooter]}>
+        <ActivityIndicator />
+        <View style={{height: offset}} />
+      </View>
+    ) : shouldRenderEndOfFeed ? (
+      <View style={{minHeight: offset}}>{renderEndOfFeed()}</View>
+    ) : (
+      <View style={{height: offset}} />
+    )
+  }, [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed, headerOffset])
 
   const scrollHandler = useAnimatedScrollHandler(onScroll || {})
   return (
diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx
index 63d9d5956..f63bc1a88 100644
--- a/src/view/com/posts/FeedErrorMessage.tsx
+++ b/src/view/com/posts/FeedErrorMessage.tsx
@@ -25,6 +25,7 @@ export enum KnownError {
   FeedgenOffline = 'FeedgenOffline',
   FeedgenUnknown = 'FeedgenUnknown',
   FeedNSFPublic = 'FeedNSFPublic',
+  FeedTooManyRequests = 'FeedTooManyRequests',
   Unknown = 'Unknown',
 }
 
@@ -100,6 +101,9 @@ function FeedgenErrorMessage({
         [KnownError.FeedgenUnknown]: _l(
           msgLingui`Hmm, some kind of issue occured when contacting the feed server. Please let the feed owner know about this issue.`,
         ),
+        [KnownError.FeedTooManyRequests]: _l(
+          msgLingui`We're sorry, but this feed is currently receiving high traffic and is temporarily unavailable. Please try again later.`,
+        ),
       }[knownError]),
     [_l, knownError],
   )
@@ -203,6 +207,13 @@ function detectKnownError(
   ) {
     return KnownError.Block
   }
+
+  // check status codes
+  if (error?.status === 429) {
+    return KnownError.FeedTooManyRequests
+  }
+
+  // convert error to string and continue
   if (typeof error !== 'string') {
     error = error.toString()
   }
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index dfb0cfcf6..b6c509e92 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -320,9 +320,15 @@ let FeedItemInner = ({
               <ContentHider
                 testID="contentHider-embed"
                 moderation={moderation.embed}
+                moderationDecisions={moderation.decisions}
                 ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
+                ignoreQuoteDecisions
                 style={styles.embed}>
-                <PostEmbeds embed={post.embed} moderation={moderation.embed} />
+                <PostEmbeds
+                  embed={post.embed}
+                  moderation={moderation.embed}
+                  moderationDecisions={moderation.decisions}
+                />
               </ContentHider>
             ) : null}
           </ContentHider>
diff --git a/src/view/com/posts/FollowingEndOfFeed.tsx b/src/view/com/posts/FollowingEndOfFeed.tsx
index 48724d8b3..6630b9a83 100644
--- a/src/view/com/posts/FollowingEndOfFeed.tsx
+++ b/src/view/com/posts/FollowingEndOfFeed.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import {StyleSheet, View, Dimensions} from 'react-native'
 import {useNavigation} from '@react-navigation/native'
 import {
   FontAwesomeIcon,
@@ -36,7 +36,12 @@ export function FollowingEndOfFeed() {
   }, [navigation])
 
   return (
-    <View style={[styles.container, pal.border]}>
+    <View
+      style={[
+        styles.container,
+        pal.border,
+        {minHeight: Dimensions.get('window').height * 0.75},
+      ]}>
       <View style={styles.inner}>
         <Text type="xl-medium" style={[s.textCenter, pal.text]}>
           You've reached the end of your feed! Find some more accounts to
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 279e00d75..21972f274 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -228,6 +228,7 @@ const styles = StyleSheet.create({
   outer: {
     borderTopWidth: 1,
     paddingHorizontal: 6,
+    paddingVertical: 4,
   },
   outerNoBorder: {
     borderTopWidth: 0,
@@ -237,7 +238,7 @@ const styles = StyleSheet.create({
     alignItems: 'center',
   },
   layoutAvi: {
-    alignSelf: 'baseline',
+    alignSelf: 'flex-start',
     width: 54,
     paddingLeft: 4,
     paddingTop: 10,
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index c1de10aa5..16e98ddf2 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -412,10 +412,12 @@ let ProfileHeaderLoaded = ({
   const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
 
   return (
-    <View style={pal.view}>
-      <UserBanner banner={profile.banner} moderation={moderation.avatar} />
-      <View style={styles.content}>
-        <View style={[styles.buttonsLine]}>
+    <View style={pal.view} pointerEvents="box-none">
+      <View pointerEvents="none">
+        <UserBanner banner={profile.banner} moderation={moderation.avatar} />
+      </View>
+      <View style={styles.content} pointerEvents="box-none">
+        <View style={[styles.buttonsLine]} pointerEvents="box-none">
           {isMe ? (
             <TouchableOpacity
               testID="profileHeaderEditProfileButton"
@@ -468,7 +470,7 @@ let ProfileHeaderLoaded = ({
                       pal.text,
                       {
                         color: showSuggestedFollows
-                          ? colors.white
+                          ? pal.textInverted.color
                           : pal.text.color,
                       },
                     ]}
@@ -525,7 +527,7 @@ let ProfileHeaderLoaded = ({
             </NativeDropdown>
           ) : undefined}
         </View>
-        <View>
+        <View pointerEvents="none">
           <Text
             testID="profileHeaderDisplayName"
             type="title-2xl"
@@ -536,7 +538,7 @@ let ProfileHeaderLoaded = ({
             )}
           </Text>
         </View>
-        <View style={styles.handleLine}>
+        <View style={styles.handleLine} pointerEvents="none">
           {profile.viewer?.followedBy && !blockHide ? (
             <View style={[styles.pill, pal.btn, s.mr5]}>
               <Text type="xs" style={[pal.text]}>
@@ -557,7 +559,7 @@ let ProfileHeaderLoaded = ({
         </View>
         {!blockHide && (
           <>
-            <View style={styles.metricsLine}>
+            <View style={styles.metricsLine} pointerEvents="box-none">
               <Link
                 testID="profileHeaderFollowersButton"
                 style={[s.flexRow, s.mr10]}
@@ -604,12 +606,14 @@ let ProfileHeaderLoaded = ({
               </Text>
             </View>
             {descriptionRT && !moderation.profile.blur ? (
-              <RichText
-                testID="profileHeaderDescription"
-                style={[styles.description, pal.text]}
-                numberOfLines={15}
-                richText={descriptionRT}
-              />
+              <View pointerEvents="none">
+                <RichText
+                  testID="profileHeaderDescription"
+                  style={[styles.description, pal.text]}
+                  numberOfLines={15}
+                  richText={descriptionRT}
+                />
+              </View>
             ) : undefined}
           </>
         )}
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index f648c9801..1d550aa5c 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -70,15 +70,19 @@ export function ProfileHeaderSuggestedFollows({
   })
 
   return (
-    <Animated.View style={[{overflow: 'hidden', opacity: 0}, animatedStyles]}>
-      <View style={{paddingVertical: OUTER_PADDING}}>
+    <Animated.View
+      pointerEvents="box-none"
+      style={[{overflow: 'hidden', opacity: 0}, animatedStyles]}>
+      <View style={{paddingVertical: OUTER_PADDING}} pointerEvents="box-none">
         <View
+          pointerEvents="box-none"
           style={{
             backgroundColor: pal.viewLight.backgroundColor,
             height: '100%',
             paddingTop: INNER_PADDING / 2,
           }}>
           <View
+            pointerEvents="box-none"
             style={{
               flexDirection: 'row',
               justifyContent: 'space-between',
diff --git a/src/view/com/util/LoadingPlaceholder.tsx b/src/view/com/util/LoadingPlaceholder.tsx
index 74e36ff7b..a07b33325 100644
--- a/src/view/com/util/LoadingPlaceholder.tsx
+++ b/src/view/com/util/LoadingPlaceholder.tsx
@@ -7,7 +7,7 @@ import {
   DimensionValue,
 } from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {HeartIcon} from 'lib/icons'
+import {HeartIcon, HeartIconSolid} from 'lib/icons'
 import {s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -46,12 +46,22 @@ export function PostLoadingPlaceholder({
   const pal = usePalette('default')
   return (
     <View style={[styles.post, pal.view, style]}>
-      <LoadingPlaceholder width={52} height={52} style={styles.avatar} />
+      <LoadingPlaceholder
+        width={52}
+        height={52}
+        style={[
+          styles.avatar,
+          {
+            position: 'relative',
+            top: -6,
+          },
+        ]}
+      />
       <View style={[s.flex1]}>
-        <LoadingPlaceholder width={100} height={8} style={[s.mb10]} />
-        <LoadingPlaceholder width={200} height={8} style={[s.mb5]} />
-        <LoadingPlaceholder width={200} height={8} style={[s.mb5]} />
-        <LoadingPlaceholder width={120} height={8} style={[s.mb10]} />
+        <LoadingPlaceholder width={100} height={6} style={{marginBottom: 10}} />
+        <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
+        <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
+        <LoadingPlaceholder width="80%" height={6} style={{marginBottom: 15}} />
         <View style={s.flexRow}>
           <View style={s.flex1}>
             <FontAwesomeIcon
@@ -90,6 +100,8 @@ export function PostFeedLoadingPlaceholder() {
       <PostLoadingPlaceholder />
       <PostLoadingPlaceholder />
       <PostLoadingPlaceholder />
+      <PostLoadingPlaceholder />
+      <PostLoadingPlaceholder />
     </View>
   )
 }
@@ -102,11 +114,23 @@ export function NotificationLoadingPlaceholder({
   const pal = usePalette('default')
   return (
     <View style={[styles.notification, pal.view, style]}>
-      <View style={[s.flexRow, s.mb10]}>
-        <LoadingPlaceholder width={30} height={30} style={styles.smallAvatar} />
+      <View style={{paddingLeft: 30, paddingRight: 10}}>
+        <HeartIconSolid
+          style={{color: pal.colors.backgroundLight} as ViewStyle}
+          size={30}
+        />
+      </View>
+      <View style={{flex: 1}}>
+        <View style={[s.flexRow, s.mb10]}>
+          <LoadingPlaceholder
+            width={30}
+            height={30}
+            style={styles.smallAvatar}
+          />
+        </View>
+        <LoadingPlaceholder width="90%" height={6} style={[s.mb5]} />
+        <LoadingPlaceholder width="70%" height={6} style={[s.mb5]} />
       </View>
-      <LoadingPlaceholder width={200} height={8} style={[s.mb5]} />
-      <LoadingPlaceholder width={120} height={8} style={[s.mb5]} />
     </View>
   )
 }
@@ -239,18 +263,19 @@ const styles = StyleSheet.create({
   },
   post: {
     flexDirection: 'row',
-    padding: 10,
-    margin: 1,
+    alignItems: 'flex-start',
+    paddingHorizontal: 10,
+    paddingTop: 20,
+    paddingBottom: 5,
   },
   avatar: {
     borderRadius: 26,
     marginRight: 10,
-    marginLeft: 6,
+    marginLeft: 8,
   },
   notification: {
+    flexDirection: 'row',
     padding: 10,
-    paddingLeft: 46,
-    margin: 1,
   },
   profileCard: {
     flexDirection: 'row',
diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx
index a13aae2b5..b1ea76621 100644
--- a/src/view/com/util/moderation/ContentHider.tsx
+++ b/src/view/com/util/moderation/ContentHider.tsx
@@ -1,36 +1,44 @@
 import React from 'react'
 import {Pressable, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ModerationUI} from '@atproto/api'
+import {ModerationUI, PostModeration} from '@atproto/api'
 import {Text} from '../text/Text'
 import {ShieldExclamation} from 'lib/icons'
 import {describeModerationCause} from 'lib/moderation'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
+import {isPostMediaBlurred} from 'lib/moderation'
 
 export function ContentHider({
   testID,
   moderation,
+  moderationDecisions,
   ignoreMute,
+  ignoreQuoteDecisions,
   style,
   childContainerStyle,
   children,
 }: React.PropsWithChildren<{
   testID?: string
   moderation: ModerationUI
+  moderationDecisions?: PostModeration['decisions']
   ignoreMute?: boolean
+  ignoreQuoteDecisions?: boolean
   style?: StyleProp<ViewStyle>
   childContainerStyle?: StyleProp<ViewStyle>
 }>) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
   const {openModal} = useModalControls()
 
-  if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) {
+  if (
+    !moderation.blur ||
+    (ignoreMute && moderation.cause?.type === 'muted') ||
+    shouldIgnoreQuote(moderationDecisions, ignoreQuoteDecisions)
+  ) {
     return (
       <View testID={testID} style={[styles.outer, style]}>
         {children}
@@ -38,6 +46,7 @@ export function ContentHider({
     )
   }
 
+  const isMute = moderation.cause?.type === 'muted'
   const desc = describeModerationCause(moderation.cause, 'content')
   return (
     <View testID={testID} style={[styles.outer, style]}>
@@ -58,7 +67,6 @@ export function ContentHider({
         accessibilityLabel=""
         style={[
           styles.cover,
-          {paddingRight: isMobile ? 22 : 18},
           moderation.noOverride
             ? {borderWidth: 1, borderColor: pal.colors.borderDark}
             : pal.viewLight,
@@ -74,14 +82,22 @@ export function ContentHider({
           accessibilityRole="button"
           accessibilityLabel={_(msg`Learn more about this warning`)}
           accessibilityHint="">
-          <ShieldExclamation size={18} style={pal.text} />
+          {isMute ? (
+            <FontAwesomeIcon
+              icon={['far', 'eye-slash']}
+              size={18}
+              color={pal.colors.textLight}
+            />
+          ) : (
+            <ShieldExclamation size={18} style={pal.textLight} />
+          )}
         </Pressable>
-        <Text type="lg" style={pal.text}>
+        <Text type="md" style={pal.text}>
           {desc.name}
         </Text>
         {!moderation.noOverride && (
           <View style={styles.showBtn}>
-            <Text type="xl" style={pal.link}>
+            <Text type="lg" style={pal.link}>
               {override ? 'Hide' : 'Show'}
             </Text>
           </View>
@@ -92,6 +108,16 @@ export function ContentHider({
   )
 }
 
+function shouldIgnoreQuote(
+  decisions: PostModeration['decisions'] | undefined,
+  ignore: boolean | undefined,
+): boolean {
+  if (!decisions || !ignore) {
+    return false
+  }
+  return !isPostMediaBlurred(decisions)
+}
+
 const styles = StyleSheet.create({
   outer: {
     overflow: 'hidden',
@@ -104,6 +130,7 @@ const styles = StyleSheet.create({
     marginTop: 4,
     paddingVertical: 14,
     paddingLeft: 14,
+    paddingRight: 18,
   },
   showBtn: {
     marginLeft: 'auto',
diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx
index c2b857f54..bffb7ea1a 100644
--- a/src/view/com/util/moderation/PostHider.tsx
+++ b/src/view/com/util/moderation/PostHider.tsx
@@ -1,8 +1,8 @@
 import React, {ComponentProps} from 'react'
-import {StyleSheet, Pressable, View} from 'react-native'
+import {StyleSheet, Pressable, View, ViewStyle, StyleProp} from 'react-native'
 import {ModerationUI} from '@atproto/api'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {Link} from '../Link'
 import {Text} from '../text/Text'
 import {addStyle} from 'lib/styles'
@@ -13,9 +13,8 @@ import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
 
 interface Props extends ComponentProps<typeof Link> {
-  // testID?: string
-  // href?: string
-  // style: StyleProp<ViewStyle>
+  iconSize: number
+  iconStyles: StyleProp<ViewStyle>
   moderation: ModerationUI
 }
 
@@ -25,11 +24,12 @@ export function PostHider({
   moderation,
   style,
   children,
+  iconSize,
+  iconStyles,
   ...props
 }: Props) {
   const pal = usePalette('default')
   const {_} = useLingui()
-  const {isMobile} = useWebMediaQueries()
   const [override, setOverride] = React.useState(false)
   const {openModal} = useModalControls()
 
@@ -47,57 +47,74 @@ export function PostHider({
     )
   }
 
+  const isMute = moderation.cause?.type === 'muted'
   const desc = describeModerationCause(moderation.cause, 'content')
-  return (
-    <>
+  return !override ? (
+    <Pressable
+      onPress={() => {
+        if (!moderation.noOverride) {
+          setOverride(v => !v)
+        }
+      }}
+      accessibilityRole="button"
+      accessibilityHint={override ? 'Hide the content' : 'Show the content'}
+      accessibilityLabel=""
+      style={[
+        styles.description,
+        override ? {paddingBottom: 0} : undefined,
+        pal.view,
+      ]}>
       <Pressable
         onPress={() => {
-          if (!moderation.noOverride) {
-            setOverride(v => !v)
-          }
+          openModal({
+            name: 'moderation-details',
+            context: 'content',
+            moderation,
+          })
         }}
         accessibilityRole="button"
-        accessibilityHint={override ? 'Hide the content' : 'Show the content'}
-        accessibilityLabel=""
-        style={[
-          styles.description,
-          {paddingRight: isMobile ? 22 : 18},
-          pal.viewLight,
-        ]}>
-        <Pressable
-          onPress={() => {
-            openModal({
-              name: 'moderation-details',
-              context: 'content',
-              moderation,
-            })
-          }}
-          accessibilityRole="button"
-          accessibilityLabel={_(msg`Learn more about this warning`)}
-          accessibilityHint="">
-          <ShieldExclamation size={18} style={pal.text} />
-        </Pressable>
-        <Text type="lg" style={[{flex: 1}, pal.text]} numberOfLines={1}>
-          {desc.name}
-        </Text>
-        {!moderation.noOverride && (
-          <Text type="xl" style={[styles.showBtn, pal.link]}>
-            {override ? 'Hide' : 'Show'}
-          </Text>
-        )}
-      </Pressable>
-      {override && (
-        <View style={[styles.childrenContainer, pal.border, pal.viewLight]}>
-          <Link
-            testID={testID}
-            style={addStyle(style, styles.child)}
-            href={href}
-            noFeedback>
-            {children}
-          </Link>
+        accessibilityLabel={_(msg`Learn more about this warning`)}
+        accessibilityHint="">
+        <View
+          style={[
+            pal.viewLight,
+            {
+              width: iconSize,
+              height: iconSize,
+              borderRadius: iconSize,
+              alignItems: 'center',
+              justifyContent: 'center',
+            },
+            iconStyles,
+          ]}>
+          {isMute ? (
+            <FontAwesomeIcon
+              icon={['far', 'eye-slash']}
+              size={14}
+              color={pal.colors.textLight}
+            />
+          ) : (
+            <ShieldExclamation size={14} style={pal.textLight} />
+          )}
         </View>
+      </Pressable>
+      <Text type="sm" style={[{flex: 1}, pal.textLight]} numberOfLines={1}>
+        {desc.name}
+      </Text>
+      {!moderation.noOverride && (
+        <Text type="sm" style={[styles.showBtn, pal.link]}>
+          {override ? 'Hide' : 'Show'}
+        </Text>
       )}
-    </>
+    </Pressable>
+  ) : (
+    <Link
+      testID={testID}
+      style={addStyle(style, styles.child)}
+      href={href}
+      noFeedback>
+      {children}
+    </Link>
   )
 }
 
@@ -106,18 +123,15 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
     gap: 4,
-    paddingVertical: 14,
-    paddingLeft: 18,
+    paddingVertical: 10,
+    paddingLeft: 6,
+    paddingRight: 18,
     marginTop: 1,
   },
   showBtn: {
     marginLeft: 'auto',
     alignSelf: 'center',
   },
-  childrenContainer: {
-    paddingHorizontal: 4,
-    paddingBottom: 6,
-  },
   child: {
     borderWidth: 0,
     borderTopWidth: 0,
diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
index d2675ca54..0f07b679b 100644
--- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
+++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx
@@ -10,6 +10,7 @@ import {
 } from 'lib/moderation'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {useModalControls} from '#/state/modals'
 
 export function ProfileHeaderAlerts({
@@ -31,6 +32,7 @@ export function ProfileHeaderAlerts({
   return (
     <View style={styles.grid}>
       {causes.map(cause => {
+        const isMute = cause.type === 'muted'
         const desc = describeModerationCause(cause, 'account')
         return (
           <Pressable
@@ -47,11 +49,19 @@ export function ProfileHeaderAlerts({
             accessibilityLabel={_(msg`Learn more about this warning`)}
             accessibilityHint=""
             style={[styles.container, pal.viewLight, style]}>
-            <ShieldExclamation style={pal.text} size={24} />
-            <Text type="lg" style={[{flex: 1}, pal.text]}>
+            {isMute ? (
+              <FontAwesomeIcon
+                icon={['far', 'eye-slash']}
+                size={14}
+                color={pal.colors.textLight}
+              />
+            ) : (
+              <ShieldExclamation style={pal.text} size={18} />
+            )}
+            <Text type="sm" style={[{flex: 1}, pal.text]}>
               {desc.name}
             </Text>
-            <Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
+            <Text type="sm" style={[pal.link, styles.learnMoreBtn]}>
               <Trans>Learn More</Trans>
             </Text>
           </Pressable>
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index ca3bf1104..5c16a3b3e 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -16,6 +16,7 @@ import {
   AppBskyFeedDefs,
   AppBskyGraphDefs,
   ModerationUI,
+  PostModeration,
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
@@ -28,8 +29,9 @@ import {getYoutubeVideoId} from 'lib/strings/url-helpers'
 import {MaybeQuoteEmbed} from './QuoteEmbed'
 import {AutoSizedImage} from '../images/AutoSizedImage'
 import {ListEmbed} from './ListEmbed'
-import {isCauseALabelOnUri} from 'lib/moderation'
+import {isCauseALabelOnUri, isQuoteBlurred} from 'lib/moderation'
 import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+import {ContentHider} from '../moderation/ContentHider'
 
 type Embed =
   | AppBskyEmbedRecord.View
@@ -41,10 +43,12 @@ type Embed =
 export function PostEmbeds({
   embed,
   moderation,
+  moderationDecisions,
   style,
 }: {
   embed?: Embed
   moderation: ModerationUI
+  moderationDecisions?: PostModeration['decisions']
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
@@ -55,14 +59,17 @@ export function PostEmbeds({
   // =
   if (AppBskyEmbedRecordWithMedia.isView(embed)) {
     const isModOnQuote =
-      AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
-      isCauseALabelOnUri(moderation.cause, embed.record.record.uri)
+      (AppBskyEmbedRecord.isViewRecord(embed.record.record) &&
+        isCauseALabelOnUri(moderation.cause, embed.record.record.uri)) ||
+      (moderationDecisions && isQuoteBlurred(moderationDecisions))
     const mediaModeration = isModOnQuote ? {} : moderation
     const quoteModeration = isModOnQuote ? moderation : {}
     return (
       <View style={[styles.stackContainer, style]}>
         <PostEmbeds embed={embed.media} moderation={mediaModeration} />
-        <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
+        <ContentHider moderation={quoteModeration}>
+          <MaybeQuoteEmbed embed={embed.record} moderation={quoteModeration} />
+        </ContentHider>
       </View>
     )
   }
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 6100d42db..d07fa0434 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {View, ActivityIndicator, StyleSheet} from 'react-native'
-import {useFocusEffect} from '@react-navigation/native'
+import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
 import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
@@ -65,6 +65,7 @@ function HomeScreenReady({
   const {hasSession} = useSession()
   const setMinimalShellMode = useSetMinimalShellMode()
   const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  const isPageFocused = useIsFocused()
   const [selectedPage, setSelectedPage] = React.useState<string>(initialPage)
 
   /**
@@ -174,7 +175,7 @@ function HomeScreenReady({
       <FeedPage
         key="1"
         testID="followingFeedPage"
-        isPageFocused={selectedPageIndex === 0}
+        isPageFocused={selectedPageIndex === 0 && isPageFocused}
         feed={homeFeedParams.mergeFeedEnabled ? 'home' : 'following'}
         feedParams={homeFeedParams}
         renderEmptyState={renderFollowingEmptyState}
@@ -185,7 +186,7 @@ function HomeScreenReady({
           <FeedPage
             key={f}
             testID="customFeedPage"
-            isPageFocused={selectedPageIndex === 1 + index}
+            isPageFocused={selectedPageIndex === 1 + index && isPageFocused}
             feed={f}
             renderEmptyState={renderCustomFeedEmptyState}
           />
@@ -201,7 +202,7 @@ function HomeScreenReady({
       tabBarPosition="top">
       <FeedPage
         testID="customFeedPage"
-        isPageFocused
+        isPageFocused={isPageFocused}
         feed={`feedgen|at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot`}
         renderEmptyState={renderCustomFeedEmptyState}
       />
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index d5e378ccb..ea19515b5 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -2,7 +2,7 @@ import React, {useMemo} from 'react'
 import {StyleSheet, View} from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
-import {msg} from '@lingui/macro'
+import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {CenteredView, FlatList} from '../com/util/Views'
@@ -36,6 +36,8 @@ import {useQueryClient} from '@tanstack/react-query'
 import {useComposerControls} from '#/state/shell/composer'
 import {listenSoftReset} from '#/state/events'
 import {truncateAndInvalidate} from '#/state/queries/util'
+import {Text} from '#/view/com/util/text/Text'
+import {usePalette} from 'lib/hooks/usePalette'
 
 interface SectionRef {
   scrollToTop: () => void
@@ -420,6 +422,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
       <View>
         <Feed
           testID="postsFeed"
+          enabled={isFocused}
           feed={feed}
           pollInterval={30e3}
           scrollElRef={scrollElRef}
@@ -428,7 +431,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
           scrollEventThrottle={1}
           renderEmptyState={renderPostsEmpty}
           headerOffset={headerHeight}
-          enabled={isFocused}
+          renderEndOfFeed={ProfileEndOfFeed}
         />
         {(isScrolledDown || hasNew) && (
           <LoadLatestBtn
@@ -442,6 +445,18 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   },
 )
 
+function ProfileEndOfFeed() {
+  const pal = usePalette('default')
+
+  return (
+    <View style={[pal.border, {paddingTop: 32, borderTopWidth: 1}]}>
+      <Text style={[pal.textLight, pal.border, {textAlign: 'center'}]}>
+        <Trans>End of feed</Trans>
+      </Text>
+    </View>
+  )
+}
+
 const styles = StyleSheet.create({
   container: {
     flexDirection: 'column',
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 659560a25..3a0bdcc0f 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -402,7 +402,7 @@ export function ProfileFeedScreenInner({
         isHeaderReady={true}
         renderHeader={renderHeader}
         onCurrentPageSelected={onCurrentPageSelected}>
-        {({onScroll, headerHeight, isScrolledDown, scrollElRef}) =>
+        {({onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused}) =>
           isPublic ? (
             <FeedSection
               ref={feedSectionRef}
@@ -413,6 +413,7 @@ export function ProfileFeedScreenInner({
               scrollElRef={
                 scrollElRef as React.MutableRefObject<FlatList<any> | null>
               }
+              isFocused={isFocused}
             />
           ) : (
             <CenteredView sideBorders style={[{paddingTop: headerHeight}]}>
@@ -492,10 +493,11 @@ interface FeedSectionProps {
   headerHeight: number
   isScrolledDown: boolean
   scrollElRef: React.MutableRefObject<FlatList<any> | null>
+  isFocused: boolean
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl(
-    {feed, onScroll, headerHeight, isScrolledDown, scrollElRef},
+    {feed, onScroll, headerHeight, isScrolledDown, scrollElRef, isFocused},
     ref,
   ) {
     const [hasNew, setHasNew] = React.useState(false)
@@ -518,6 +520,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
     return (
       <View>
         <Feed
+          enabled={isFocused}
           feed={feed}
           pollInterval={30e3}
           scrollElRef={scrollElRef}
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 1396b8269..3e568c8cc 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -56,6 +56,12 @@ import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
 import {isWeb} from '#/platform/detection'
 import {truncateAndInvalidate} from '#/state/queries/util'
+import {
+  usePreferencesQuery,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+} from '#/state/queries/preferences'
+import {logger} from '#/logger'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -129,7 +135,6 @@ function ProfileListScreenLoaded({
       list,
       onChange() {
         if (isCurateList) {
-          // TODO(eric) should construct these strings with a fn too
           truncateAndInvalidate(queryClient, FEED_RQKEY(`list|${list.uri}`))
         }
       },
@@ -159,7 +164,13 @@ function ProfileListScreenLoaded({
           isHeaderReady={true}
           renderHeader={renderHeader}
           onCurrentPageSelected={onCurrentPageSelected}>
-          {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
+          {({
+            onScroll,
+            headerHeight,
+            isScrolledDown,
+            scrollElRef,
+            isFocused,
+          }) => (
             <FeedSection
               ref={feedSectionRef}
               feed={`list|${uri}`}
@@ -169,6 +180,7 @@ function ProfileListScreenLoaded({
               onScroll={onScroll}
               headerHeight={headerHeight}
               isScrolledDown={isScrolledDown}
+              isFocused={isFocused}
             />
           )}
           {({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
@@ -247,19 +259,31 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const listDeleteMutation = useListDeleteMutation()
   const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
   const isModList = list.purpose === 'app.bsky.graph.defs#modlist'
-  const isPinned = false // TODO
   const isBlocking = !!list.viewer?.blocked
   const isMuting = !!list.viewer?.muted
   const isOwner = list.creator.did === currentAccount?.did
+  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
+  const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
+    useUnpinFeedMutation()
+  const isPending = isPinPending || isUnpinPending
+  const {data: preferences} = usePreferencesQuery()
 
-  const onTogglePinned = useCallback(async () => {
+  const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
+
+  const onTogglePinned = React.useCallback(async () => {
     Haptics.default()
-    // TODO
-    // list.togglePin().catch(e => {
-    //   Toast.show('There was an issue contacting the server')
-    //   logger.error('Failed to toggle pinned list', {error: e})
-    // })
-  }, [])
+
+    try {
+      if (isPinned) {
+        await unpinFeed({uri: list.uri})
+      } else {
+        await pinFeed({uri: list.uri})
+      }
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to toggle pinned feed', {error: e})
+    }
+  }, [list.uri, isPinned, pinFeed, unpinFeed])
 
   const onSubscribeMute = useCallback(() => {
     openModal({
@@ -466,10 +490,11 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       avatarType="list">
       {isCurateList || isPinned ? (
         <Button
-          testID={list.isPinned ? 'unpinBtn' : 'pinBtn'}
-          type={list.isPinned ? 'default' : 'inverted'}
-          label={list.isPinned ? 'Unpin' : 'Pin to home'}
+          testID={isPinned ? 'unpinBtn' : 'pinBtn'}
+          type={isPinned ? 'default' : 'inverted'}
+          label={isPinned ? 'Unpin' : 'Pin to home'}
           onPress={onTogglePinned}
+          disabled={isPending}
         />
       ) : isModList ? (
         isBlocking ? (
@@ -519,10 +544,11 @@ interface FeedSectionProps {
   headerHeight: number
   isScrolledDown: boolean
   scrollElRef: React.MutableRefObject<FlatList<any> | null>
+  isFocused: boolean
 }
 const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
   function FeedSectionImpl(
-    {feed, scrollElRef, onScroll, headerHeight, isScrolledDown},
+    {feed, scrollElRef, onScroll, headerHeight, isScrolledDown, isFocused},
     ref,
   ) {
     const queryClient = useQueryClient()
@@ -545,6 +571,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
       <View>
         <Feed
           testID="listFeed"
+          enabled={isFocused}
           feed={feed}
           pollInterval={30e3}
           scrollElRef={scrollElRef}
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index c7b5d1d2e..43dc28159 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -1,5 +1,6 @@
 import * as React from 'react'
 import {View} from 'react-native'
+import {PWI_ENABLED} from '#/lib/build-flags'
 
 // Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
 // MIT License
@@ -99,7 +100,7 @@ function NativeStackNavigator({
   const {showLoggedOut} = useLoggedOutView()
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const {isMobile} = useWebMediaQueries()
-  if (activeRouteRequiresAuth && !hasSession) {
+  if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) {
     return <LoggedOut />
   }
   if (showLoggedOut) {