about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/strings/capitalize.ts3
-rw-r--r--src/screens/Onboarding/StepInterests/InterestButton.tsx6
-rw-r--r--src/screens/Onboarding/StepInterests/data.ts36
-rw-r--r--src/screens/Onboarding/StepInterests/index.tsx13
-rw-r--r--src/screens/Onboarding/StepSuggestedAccounts/index.tsx8
-rw-r--r--src/screens/Onboarding/StepTopicalFeeds.tsx8
-rw-r--r--src/screens/Onboarding/index.tsx35
-rw-r--r--src/screens/Onboarding/state.ts40
-rw-r--r--src/screens/Onboarding/util.ts10
-rw-r--r--src/state/modals/index.tsx2
-rw-r--r--src/state/queries/feed.ts4
-rw-r--r--src/state/queries/notifications/util.ts47
-rw-r--r--src/state/queries/post-feed.ts13
-rw-r--r--src/state/queries/profile.ts20
-rw-r--r--src/view/com/modals/AltImage.tsx25
-rw-r--r--src/view/com/modals/ProfilePreview.tsx4
-rw-r--r--src/view/com/notifications/FeedItem.tsx4
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx1
-rw-r--r--src/view/com/util/UserInfoText.tsx6
-rw-r--r--src/view/screens/Feeds.tsx4
-rw-r--r--src/view/screens/Profile.tsx18
21 files changed, 225 insertions, 82 deletions
diff --git a/src/lib/strings/capitalize.ts b/src/lib/strings/capitalize.ts
new file mode 100644
index 000000000..a3415bd00
--- /dev/null
+++ b/src/lib/strings/capitalize.ts
@@ -0,0 +1,3 @@
+export function capitalize(str: string) {
+  return str.charAt(0).toUpperCase() + str.slice(1)
+}
diff --git a/src/screens/Onboarding/StepInterests/InterestButton.tsx b/src/screens/Onboarding/StepInterests/InterestButton.tsx
index 02413b18d..cc692dafd 100644
--- a/src/screens/Onboarding/StepInterests/InterestButton.tsx
+++ b/src/screens/Onboarding/StepInterests/InterestButton.tsx
@@ -4,11 +4,13 @@ import {View, ViewStyle, TextStyle} from 'react-native'
 import {useTheme, atoms as a, native} from '#/alf'
 import * as Toggle from '#/components/forms/Toggle'
 import {Text} from '#/components/Typography'
+import {capitalize} from '#/lib/strings/capitalize'
 
-import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
+import {Context} from '#/screens/Onboarding/state'
 
 export function InterestButton({interest}: {interest: string}) {
   const t = useTheme()
+  const {interestsDisplayNames} = React.useContext(Context)
   const ctx = Toggle.useItemContext()
 
   const styles = React.useMemo(() => {
@@ -72,7 +74,7 @@ export function InterestButton({interest}: {interest: string}) {
           native({paddingTop: 2}),
           ctx.selected ? styles.textSelected : {},
         ]}>
-        {INTEREST_TO_DISPLAY_NAME[interest]}
+        {interestsDisplayNames[interest] || capitalize(interest)}
       </Text>
     </View>
   )
diff --git a/src/screens/Onboarding/StepInterests/data.ts b/src/screens/Onboarding/StepInterests/data.ts
deleted file mode 100644
index 00a25331c..000000000
--- a/src/screens/Onboarding/StepInterests/data.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-export const INTEREST_TO_DISPLAY_NAME: {
-  [key: string]: string
-} = {
-  news: 'News',
-  journalism: 'Journalism',
-  nature: 'Nature',
-  art: 'Art',
-  comics: 'Comics',
-  writers: 'Writers',
-  culture: 'Culture',
-  sports: 'Sports',
-  pets: 'Pets',
-  animals: 'Animals',
-  books: 'Books',
-  education: 'Education',
-  climate: 'Climate',
-  science: 'Science',
-  politics: 'Politics',
-  fitness: 'Fitness',
-  tech: 'Tech',
-  dev: 'Software Dev',
-  comedy: 'Comedy',
-  gaming: 'Video Games',
-  food: 'Food',
-  cooking: 'Cooking',
-}
-
-export type ApiResponseMap = {
-  interests: string[]
-  suggestedAccountDids: {
-    [key: string]: string[]
-  }
-  suggestedFeedUris: {
-    [key: string]: string[]
-  }
-}
diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx
index 6f60991d5..5440dcd2b 100644
--- a/src/screens/Onboarding/StepInterests/index.tsx
+++ b/src/screens/Onboarding/StepInterests/index.tsx
@@ -17,17 +17,14 @@ import {getAgent} from '#/state/session'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {Text} from '#/components/Typography'
 import {useOnboardingDispatch} from '#/state/shell'
+import {capitalize} from '#/lib/strings/capitalize'
 
-import {Context} from '#/screens/Onboarding/state'
+import {Context, ApiResponseMap} from '#/screens/Onboarding/state'
 import {
   Title,
   Description,
   OnboardingControls,
 } from '#/screens/Onboarding/Layout'
-import {
-  ApiResponseMap,
-  INTEREST_TO_DISPLAY_NAME,
-} from '#/screens/Onboarding/StepInterests/data'
 import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
 import {IconCircle} from '#/screens/Onboarding/IconCircle'
 
@@ -36,7 +33,7 @@ export function StepInterests() {
   const t = useTheme()
   const {track} = useAnalytics()
   const {gtMobile} = useBreakpoints()
-  const {state, dispatch} = React.useContext(Context)
+  const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
   const [saving, setSaving] = React.useState(false)
   const [interests, setInterests] = React.useState<string[]>(
     state.interestsStepResults.selectedInterests.map(i => i),
@@ -202,7 +199,9 @@ export function StepInterests() {
                 <Toggle.Item
                   key={interest}
                   name={interest}
-                  label={INTEREST_TO_DISPLAY_NAME[interest]}>
+                  label={
+                    interestsDisplayNames[interest] || capitalize(interest)
+                  }>
                   <InterestButton interest={interest} />
                 </Toggle.Item>
               ))}
diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
index d3831791c..cf1f82559 100644
--- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
+++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
@@ -14,6 +14,7 @@ import {Loader} from '#/components/Loader'
 import * as Toggle from '#/components/forms/Toggle'
 import {useModerationOpts} from '#/state/queries/preferences'
 import {useAnalytics} from '#/lib/analytics/analytics'
+import {capitalize} from '#/lib/strings/capitalize'
 
 import {Context} from '#/screens/Onboarding/state'
 import {
@@ -25,7 +26,6 @@ import {
   SuggestedAccountCard,
   SuggestedAccountCardPlaceholder,
 } from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard'
-import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
 import {aggregateInterestItems} from '#/screens/Onboarding/util'
 import {IconCircle} from '#/screens/Onboarding/IconCircle'
 
@@ -70,7 +70,7 @@ export function Inner({
 export function StepSuggestedAccounts() {
   const {_} = useLingui()
   const {track} = useAnalytics()
-  const {state, dispatch} = React.useContext(Context)
+  const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
   const {gtMobile} = useBreakpoints()
   const suggestedDids = React.useMemo(() => {
     return aggregateInterestItems(
@@ -93,10 +93,10 @@ export function StepSuggestedAccounts() {
 
   const interestsText = React.useMemo(() => {
     const i = state.interestsStepResults.selectedInterests.map(
-      i => INTEREST_TO_DISPLAY_NAME[i],
+      i => interestsDisplayNames[i] || capitalize(i),
     )
     return i.join(', ')
-  }, [state.interestsStepResults.selectedInterests])
+  }, [state.interestsStepResults.selectedInterests, interestsDisplayNames])
 
   const handleContinue = React.useCallback(async () => {
     setSaving(true)
diff --git a/src/screens/Onboarding/StepTopicalFeeds.tsx b/src/screens/Onboarding/StepTopicalFeeds.tsx
index 516c18e6e..ef77cc128 100644
--- a/src/screens/Onboarding/StepTopicalFeeds.tsx
+++ b/src/screens/Onboarding/StepTopicalFeeds.tsx
@@ -10,6 +10,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import * as Toggle from '#/components/forms/Toggle'
 import {Loader} from '#/components/Loader'
 import {useAnalytics} from '#/lib/analytics/analytics'
+import {capitalize} from '#/lib/strings/capitalize'
 
 import {Context} from '#/screens/Onboarding/state'
 import {
@@ -18,14 +19,13 @@ import {
   OnboardingControls,
 } from '#/screens/Onboarding/Layout'
 import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
-import {INTEREST_TO_DISPLAY_NAME} from '#/screens/Onboarding/StepInterests/data'
 import {aggregateInterestItems} from '#/screens/Onboarding/util'
 import {IconCircle} from '#/screens/Onboarding/IconCircle'
 
 export function StepTopicalFeeds() {
   const {_} = useLingui()
   const {track} = useAnalytics()
-  const {state, dispatch} = React.useContext(Context)
+  const {state, dispatch, interestsDisplayNames} = React.useContext(Context)
   const [selectedFeedUris, setSelectedFeedUris] = React.useState<string[]>([])
   const [saving, setSaving] = React.useState(false)
   const suggestedFeedUris = React.useMemo(() => {
@@ -38,10 +38,10 @@ export function StepTopicalFeeds() {
 
   const interestsText = React.useMemo(() => {
     const i = state.interestsStepResults.selectedInterests.map(
-      i => INTEREST_TO_DISPLAY_NAME[i],
+      i => interestsDisplayNames[i] || capitalize(i),
     )
     return i.join(', ')
-  }, [state.interestsStepResults.selectedInterests])
+  }, [state.interestsStepResults.selectedInterests, interestsDisplayNames])
 
   const saveFeeds = React.useCallback(async () => {
     setSaving(true)
diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx
index a4eb04012..9e5029e87 100644
--- a/src/screens/Onboarding/index.tsx
+++ b/src/screens/Onboarding/index.tsx
@@ -1,4 +1,6 @@
 import React from 'react'
+import {useLingui} from '@lingui/react'
+import {msg} from '@lingui/macro'
 
 import {Portal} from '#/components/Portal'
 
@@ -13,13 +15,44 @@ import {StepFinished} from '#/screens/Onboarding/StepFinished'
 import {StepModeration} from '#/screens/Onboarding/StepModeration'
 
 export function Onboarding() {
+  const {_} = useLingui()
   const [state, dispatch] = React.useReducer(reducer, {...initialState})
 
+  const interestsDisplayNames = React.useMemo(() => {
+    return {
+      news: _(msg`News`),
+      journalism: _(msg`Journalism`),
+      nature: _(msg`Nature`),
+      art: _(msg`Art`),
+      comics: _(msg`Comics`),
+      writers: _(msg`Writers`),
+      culture: _(msg`Culture`),
+      sports: _(msg`Sports`),
+      pets: _(msg`Pets`),
+      animals: _(msg`Animals`),
+      books: _(msg`Books`),
+      education: _(msg`Education`),
+      climate: _(msg`Climate`),
+      science: _(msg`Science`),
+      politics: _(msg`Politics`),
+      fitness: _(msg`Fitness`),
+      tech: _(msg`Tech`),
+      dev: _(msg`Software Dev`),
+      comedy: _(msg`Comedy`),
+      gaming: _(msg`Video Games`),
+      food: _(msg`Food`),
+      cooking: _(msg`Cooking`),
+    }
+  }, [_])
+
   return (
     <Portal>
       <OnboardingControls.Provider>
         <Context.Provider
-          value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}>
+          value={React.useMemo(
+            () => ({state, dispatch, interestsDisplayNames}),
+            [state, dispatch, interestsDisplayNames],
+          )}>
           <Layout>
             {state.activeStep === 'interests' && <StepInterests />}
             {state.activeStep === 'suggestedAccounts' && (
diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts
index 164c2f5f3..bd8205ca2 100644
--- a/src/screens/Onboarding/state.ts
+++ b/src/screens/Onboarding/state.ts
@@ -1,6 +1,5 @@
 import React from 'react'
 
-import {ApiResponseMap} from '#/screens/Onboarding/StepInterests/data'
 import {logger} from '#/logger'
 
 export type OnboardingState = {
@@ -59,6 +58,16 @@ export type OnboardingAction =
       feedUris: string[]
     }
 
+export type ApiResponseMap = {
+  interests: string[]
+  suggestedAccountDids: {
+    [key: string]: string[]
+  }
+  suggestedFeedUris: {
+    [key: string]: string[]
+  }
+}
+
 export const initialState: OnboardingState = {
   hasPrev: false,
   totalSteps: 7,
@@ -84,12 +93,41 @@ export const initialState: OnboardingState = {
   },
 }
 
+export const INTEREST_TO_DISPLAY_NAME_DEFAULTS: {
+  [key: string]: string
+} = {
+  news: 'News',
+  journalism: 'Journalism',
+  nature: 'Nature',
+  art: 'Art',
+  comics: 'Comics',
+  writers: 'Writers',
+  culture: 'Culture',
+  sports: 'Sports',
+  pets: 'Pets',
+  animals: 'Animals',
+  books: 'Books',
+  education: 'Education',
+  climate: 'Climate',
+  science: 'Science',
+  politics: 'Politics',
+  fitness: 'Fitness',
+  tech: 'Tech',
+  dev: 'Software Dev',
+  comedy: 'Comedy',
+  gaming: 'Video Games',
+  food: 'Food',
+  cooking: 'Cooking',
+}
+
 export const Context = React.createContext<{
   state: OnboardingState
   dispatch: React.Dispatch<OnboardingAction>
+  interestsDisplayNames: {[key: string]: string}
 }>({
   state: {...initialState},
   dispatch: () => {},
+  interestsDisplayNames: INTEREST_TO_DISPLAY_NAME_DEFAULTS,
 })
 
 export function reducer(
diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts
index 2a709a67b..eae661aa4 100644
--- a/src/screens/Onboarding/util.ts
+++ b/src/screens/Onboarding/util.ts
@@ -31,7 +31,15 @@ export function aggregateInterestItems(
   const selected = interests.length
   const all = interests
     .map(i => {
-      const suggestions = shuffle(map[i])
+      // suggestions from server
+      const rawSuggestions = map[i]
+
+      // safeguard against a missing interest->suggestion mapping
+      if (!rawSuggestions || !rawSuggestions.length) {
+        return []
+      }
+
+      const suggestions = shuffle(rawSuggestions)
 
       if (selected === 1) {
         return suggestions // return all
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 45856e108..ab710a3d0 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -6,7 +6,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
 import {ImageModel} from '#/state/models/media/image'
 import {GalleryModel} from '#/state/models/media/gallery'
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
-import {EmbedPlayerSource} from '#/lib/strings/embed-player.ts'
+import {EmbedPlayerSource} from '#/lib/strings/embed-player'
 import {ThreadgateSetting} from '../queries/threadgate'
 
 export interface ConfirmModal {
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 4acc7179a..67294ece2 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -136,6 +136,10 @@ export function getFeedTypeFromUri(uri: string) {
   return pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list'
 }
 
+export function getAvatarTypeFromUri(uri: string) {
+  return getFeedTypeFromUri(uri) === 'feed' ? 'algo' : 'list'
+}
+
 export function useFeedSourceInfoQuery({uri}: {uri: string}) {
   const type = getFeedTypeFromUri(uri)
 
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index 411a0f791..e53a07258 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -6,6 +6,7 @@ import {
   AppBskyFeedPost,
   AppBskyFeedRepost,
   AppBskyFeedLike,
+  AppBskyEmbedRecord,
 } from '@atproto/api'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import chunk from 'lodash.chunk'
@@ -110,8 +111,6 @@ function shouldFilterNotif(
       return true
     }
   }
-  // TODO: thread muting is not being applied
-  // (this requires fetching the post)
   return false
 }
 
@@ -221,10 +220,44 @@ function getSubjectUri(
   }
 }
 
-function isThreadMuted(notif: FeedNotification, mutes: string[]): boolean {
-  if (!notif.subject) {
-    return false
+export function isThreadMuted(notif: FeedNotification, threadMutes: string[]) {
+  // If there's a subject we want to use that. This will always work on the notifications tab
+  if (notif.subject) {
+    const record = notif.subject.record as AppBskyFeedPost.Record
+    // Check for a quote record
+    if (
+      (record.reply && threadMutes.includes(record.reply.root.uri)) ||
+      (notif.subject.uri && threadMutes.includes(notif.subject.uri))
+    ) {
+      return true
+    } else if (
+      AppBskyEmbedRecord.isMain(record.embed) &&
+      threadMutes.includes(record.embed.record.uri)
+    ) {
+      return true
+    }
+  } else {
+    // Otherwise we just do the best that we can
+    const record = notif.notification.record
+    if (AppBskyFeedPost.isRecord(record)) {
+      if (record.reply && threadMutes.includes(record.reply.root.uri)) {
+        // We can always filter replies
+        return true
+      } else if (
+        AppBskyEmbedRecord.isMain(record.embed) &&
+        threadMutes.includes(record.embed.record.uri)
+      ) {
+        // We can also filter quotes if the quoted post is the root
+        return true
+      }
+    } else if (
+      AppBskyFeedRepost.isRecord(record) &&
+      threadMutes.includes(record.subject.uri)
+    ) {
+      // Finally we can filter reposts, again if the post is the root
+      return true
+    }
   }
-  const record = notif.subject.record as AppBskyFeedPost.Record // assured in fetchSubjects()
-  return mutes.includes(record.reply?.root.uri || notif.subject.uri)
+
+  return false
 }
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 82acf3974..b422fa8fe 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -28,6 +28,7 @@ import {getModerationOpts} from '#/state/queries/preferences/moderation'
 import {KnownError} from '#/view/com/posts/FeedErrorMessage'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
 import {useModerationOpts} from './preferences'
+import {queryClient} from 'lib/react-query'
 
 type ActorDid = string
 type AuthorFilter =
@@ -444,3 +445,15 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
     throw new Error(KnownError.FeedNSFPublic)
   }
 }
+
+export function resetProfilePostsQueries(did: string, timeout = 0) {
+  setTimeout(() => {
+    queryClient.resetQueries({
+      predicate: query =>
+        !!(
+          query.queryKey[0] === 'post-feed' &&
+          (query.queryKey[1] as string)?.includes(did)
+        ),
+    })
+  }, timeout)
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 74be99330..affb8295c 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -17,6 +17,7 @@ import {updateProfileShadow} from '../cache/profile-shadow'
 import {uploadBlob} from '#/lib/api'
 import {until} from '#/lib/async/until'
 import {Shadow} from '#/state/cache/types'
+import {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
 import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
 import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
@@ -26,16 +27,19 @@ import {track} from '#/lib/analytics/analytics'
 export const RQKEY = (did: string) => ['profile', did]
 export const profilesQueryKey = (handles: string[]) => ['profiles', handles]
 
-export function useProfileQuery({did}: {did: string | undefined}) {
-  const {currentAccount} = useSession()
-  const isCurrentAccount = did === currentAccount?.did
-
+export function useProfileQuery({
+  did,
+  staleTime = STALE.SECONDS.FIFTEEN,
+}: {
+  did: string | undefined
+  staleTime?: number
+}) {
   return useQuery({
     // WARNING
     // this staleTime is load-bearing
     // if you remove it, the UI infinite-loops
     // -prf
-    staleTime: isCurrentAccount ? STALE.SECONDS.THIRTY : STALE.MINUTES.FIVE,
+    staleTime,
     refetchOnWindowFocus: true,
     queryKey: RQKEY(did || ''),
     queryFn: async () => {
@@ -375,8 +379,9 @@ function useProfileBlockMutation() {
         {subject: did, createdAt: new Date().toISOString()},
       )
     },
-    onSuccess() {
+    onSuccess(_, {did}) {
       queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
+      resetProfilePostsQueries(did, 1000)
     },
   })
 }
@@ -394,6 +399,9 @@ function useProfileUnblockMutation() {
         rkey,
       })
     },
+    onSuccess(_, {did}) {
+      resetProfilePostsQueries(did, 1000)
+    },
   })
 }
 
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
index 5156511d6..7671c29c8 100644
--- a/src/view/com/modals/AltImage.tsx
+++ b/src/view/com/modals/AltImage.tsx
@@ -4,7 +4,9 @@ import {
   StyleSheet,
   TouchableOpacity,
   View,
+  TextInput as RNTextInput,
   useWindowDimensions,
+  ScrollView as RNScrollView,
 } from 'react-native'
 import {ScrollView, TextInput} from './util'
 import {Image} from 'expo-image'
@@ -13,6 +15,7 @@ import {gradients, s} from 'lib/styles'
 import {enforceLen} from 'lib/strings/helpers'
 import {MAX_ALT_TEXT} from 'lib/constants'
 import {useTheme} from 'lib/ThemeContext'
+import {useIsKeyboardVisible} from 'lib/hooks/useIsKeyboardVisible'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
 import {isWeb} from 'platform/detection'
@@ -34,6 +37,24 @@ export function Component({image}: Props) {
   const [altText, setAltText] = useState(image.altText)
   const windim = useWindowDimensions()
   const {closeModal} = useModalControls()
+  const inputRef = React.useRef<RNTextInput>(null)
+  const scrollViewRef = React.useRef<RNScrollView>(null)
+  const keyboardShown = useIsKeyboardVisible()
+
+  // Autofocus hack when we open the modal. We have to wait for the animation to complete first
+  React.useEffect(() => {
+    setTimeout(() => {
+      inputRef.current?.focus()
+    }, 500)
+  }, [])
+
+  // We'd rather be at the bottom here so that we can easily dismiss the modal instead of having to scroll
+  // (especially on android, it acts weird)
+  React.useEffect(() => {
+    if (keyboardShown[0]) {
+      scrollViewRef.current?.scrollToEnd()
+    }
+  }, [keyboardShown])
 
   const imageStyles = useMemo<ImageStyle>(() => {
     const maxWidth = isWeb ? 450 : windim.width
@@ -71,6 +92,7 @@ export function Component({image}: Props) {
       testID="altTextImageModal"
       style={[pal.view, styles.scrollContainer]}
       keyboardShouldPersistTaps="always"
+      ref={scrollViewRef}
       nativeID="imageAltText">
       <View style={styles.scrollInner}>
         <View style={[pal.viewLight, styles.imageContainer]}>
@@ -97,7 +119,8 @@ export function Component({image}: Props) {
           accessibilityLabel={_(msg`Image alt text`)}
           accessibilityHint=""
           accessibilityLabelledBy="imageAltText"
-          autoFocus
+          // @ts-ignore This is fine, type is weird on the BottomSheetTextInput
+          ref={inputRef}
         />
         <View style={styles.buttonControls}>
           <TouchableOpacity
diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx
index 77e68db70..88b0df71d 100644
--- a/src/view/com/modals/ProfilePreview.tsx
+++ b/src/view/com/modals/ProfilePreview.tsx
@@ -27,12 +27,12 @@ export function Component({did}: {did: string}) {
     data: profile,
     error: profileError,
     refetch: refetchProfile,
-    isFetching: isFetchingProfile,
+    isLoading: isLoadingProfile,
   } = useProfileQuery({
     did: did,
   })
 
-  if (isFetchingProfile || !moderationOpts) {
+  if (isLoadingProfile || !moderationOpts) {
     return (
       <CenteredView style={[pal.view, s.flex1]}>
         <ProfileHeader
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 0dfac2a83..f037097df 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -167,9 +167,7 @@ let FeedItem = ({
     icon = 'user-plus'
     iconStyle = [s.blue3 as FontAwesomeIconStyle]
   } else if (item.type === 'feedgen-like') {
-    action = item.subjectUri
-      ? _(msg`liked your custom feed '${new AtUri(item.subjectUri).rkey}'`)
-      : _(msg`liked your custom feed`)
+    action = _(msg`liked your custom feed`)
     icon = 'HeartIconSolid'
     iconStyle = [
       s.likeColor as FontAwesomeIconStyle,
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 1cc2bb824..c5912376e 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -775,6 +775,7 @@ const useStyles = () => {
     },
     postTextLargeContainer: {
       paddingHorizontal: 0,
+      paddingRight: 0,
       paddingBottom: 10,
     },
     translateLink: {
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index e5d2ceb03..9cb9997f6 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -9,6 +9,7 @@ import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
 import {useProfileQuery} from '#/state/queries/profile'
+import {STALE} from '#/state/queries'
 
 export function UserInfoText({
   type = 'md',
@@ -29,7 +30,10 @@ export function UserInfoText({
   attr = attr || 'handle'
   failed = failed || 'user'
 
-  const {data: profile, isError} = useProfileQuery({did})
+  const {data: profile, isError} = useProfileQuery({
+    did,
+    staleTime: STALE.INFINITY,
+  })
 
   let inner
   if (isError) {
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 9b3fd6181..6651084bd 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -30,6 +30,7 @@ import {
   useFeedSourceInfoQuery,
   useGetPopularFeedsQuery,
   useSearchPopularFeedsMutation,
+  getAvatarTypeFromUri,
 } from '#/state/queries/feed'
 import {cleanError} from 'lib/strings/errors'
 import {useComposerControls} from '#/state/shell/composer'
@@ -555,6 +556,7 @@ function SavedFeed({feedUri}: {feedUri: string}) {
   const pal = usePalette('default')
   const {isMobile} = useWebMediaQueries()
   const {data: info, error} = useFeedSourceInfoQuery({uri: feedUri})
+  const typeAvatar = getAvatarTypeFromUri(feedUri)
 
   if (!info)
     return (
@@ -582,7 +584,7 @@ function SavedFeed({feedUri}: {feedUri: string}) {
           />
         </View>
       ) : (
-        <UserAvatar type="algo" size={28} avatar={info.avatar} />
+        <UserAvatar type={typeAvatar} size={28} avatar={info.avatar} />
       )}
       <View
         style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 7fc4d7a20..6d0f15d81 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -21,7 +21,10 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {ComposeIcon2} from 'lib/icons'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {combinedDisplayName} from 'lib/strings/display-names'
-import {FeedDescriptor} from '#/state/queries/post-feed'
+import {
+  FeedDescriptor,
+  resetProfilePostsQueries,
+} from '#/state/queries/post-feed'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -55,13 +58,13 @@ export function ProfileScreen({route}: Props) {
     data: resolvedDid,
     error: resolveError,
     refetch: refetchDid,
-    isInitialLoading: isInitialLoadingDid,
+    isLoading: isLoadingDid,
   } = useResolveDidQuery(name)
   const {
     data: profile,
     error: profileError,
     refetch: refetchProfile,
-    isInitialLoading: isInitialLoadingProfile,
+    isLoading: isLoadingProfile,
   } = useProfileQuery({
     did: resolvedDid,
   })
@@ -74,7 +77,14 @@ export function ProfileScreen({route}: Props) {
     }
   }, [resolveError, refetchDid, refetchProfile])
 
-  if (isInitialLoadingDid || isInitialLoadingProfile || !moderationOpts) {
+  // When we open the profile, we want to reset the posts query if we are blocked.
+  React.useEffect(() => {
+    if (resolvedDid && profile?.viewer?.blockedBy) {
+      resetProfilePostsQueries(resolvedDid)
+    }
+  }, [profile?.viewer?.blockedBy, resolvedDid])
+
+  if (isLoadingDid || isLoadingProfile || !moderationOpts) {
     return (
       <CenteredView>
         <ProfileHeader