about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/alf/atoms.ts12
-rw-r--r--src/components/FeedInterstitials.tsx2
-rw-r--r--src/components/ProfileCard.tsx7
-rw-r--r--src/components/ProgressGuide/FollowDialog.tsx829
-rw-r--r--src/components/ProgressGuide/List.tsx44
-rw-r--r--src/components/ProgressGuide/Task.tsx14
-rw-r--r--src/components/dms/dialogs/SearchablePeopleList.tsx3
-rw-r--r--src/lib/statsig/events.ts2
-rw-r--r--src/lib/statsig/gates.ts5
-rw-r--r--src/screens/Onboarding/StepFinished.tsx8
-rw-r--r--src/screens/Onboarding/state.ts13
-rw-r--r--src/state/queries/actor-search.ts26
-rw-r--r--src/state/queries/suggested-follows.ts9
-rw-r--r--src/state/shell/progress-guide.tsx96
-rw-r--r--src/view/com/posts/PostFeed.tsx6
-rw-r--r--src/view/com/util/List.tsx6
-rw-r--r--src/view/com/util/Toast.tsx4
17 files changed, 1039 insertions, 47 deletions
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index ad4929ec8..df2b29d8a 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -302,6 +302,18 @@ export const atoms = {
   border_0: {
     borderWidth: 0,
   },
+  border_t_0: {
+    borderTopWidth: 0,
+  },
+  border_b_0: {
+    borderBottomWidth: 0,
+  },
+  border_l_0: {
+    borderLeftWidth: 0,
+  },
+  border_r_0: {
+    borderRightWidth: 0,
+  },
   border: {
     borderWidth: StyleSheet.hairlineWidth,
   },
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 85ed58280..ec224eeae 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -280,8 +280,8 @@ export function ProfileGrid({
                     profile={profile}
                     moderationOpts={moderationOpts}
                     logContext="FeedInterstitial"
-                    color="secondary_inverted"
                     shape="round"
+                    colorInverted
                   />
                 </ProfileCard.Header>
                 <ProfileCard.Description profile={profile} numberOfLines={2} />
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index 668bd0f3c..7bec14b9c 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -285,6 +285,7 @@ export type FollowButtonProps = {
   moderationOpts: ModerationOpts
   logContext: LogEvents['profile:follow']['logContext'] &
     LogEvents['profile:unfollow']['logContext']
+  colorInverted?: boolean
 } & Partial<ButtonProps>
 
 export function FollowButton(props: FollowButtonProps) {
@@ -297,6 +298,8 @@ export function FollowButtonInner({
   profile: profileUnshadowed,
   moderationOpts,
   logContext,
+  onPress: onPressProp,
+  colorInverted,
   ...rest
 }: FollowButtonProps) {
   const {_} = useLingui()
@@ -321,6 +324,7 @@ export function FollowButtonInner({
           )}`,
         ),
       )
+      onPressProp?.(e)
     } catch (err: any) {
       if (err?.name !== 'AbortError') {
         Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -341,6 +345,7 @@ export function FollowButtonInner({
           )}`,
         ),
       )
+      onPressProp?.(e)
     } catch (err: any) {
       if (err?.name !== 'AbortError') {
         Toast.show(_(msg`An issue occurred, please try again.`), 'xmark')
@@ -387,7 +392,7 @@ export function FollowButtonInner({
           label={followLabel}
           size="small"
           variant="solid"
-          color="primary"
+          color={colorInverted ? 'secondary_inverted' : 'primary'}
           {...rest}
           onPress={onPressFollow}>
           <ButtonIcon icon={Plus} position={isRound ? undefined : 'left'} />
diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx
new file mode 100644
index 000000000..6ac3200df
--- /dev/null
+++ b/src/components/ProgressGuide/FollowDialog.tsx
@@ -0,0 +1,829 @@
+import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
+import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native'
+import Animated, {
+  LayoutAnimationConfig,
+  LinearTransition,
+  ZoomInEasyDown,
+} from 'react-native-reanimated'
+import {AppBskyActorDefs, ModerationOpts} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {isWeb} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useActorSearchPaginated} from '#/state/queries/actor-search'
+import {usePreferencesQuery} from '#/state/queries/preferences'
+import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
+import {useSession} from '#/state/session'
+import {Follow10ProgressGuide} from '#/state/shell/progress-guide'
+import {ListMethods} from '#/view/com/util/List'
+import {
+  popularInterests,
+  useInterestsDisplayNames,
+} from '#/screens/Onboarding/state'
+import {
+  atoms as a,
+  native,
+  tokens,
+  useBreakpoints,
+  useTheme,
+  ViewStyleProp,
+  web,
+} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {useInteractionState} from '#/components/hooks/useInteractionState'
+import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
+import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
+import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
+import * as ProfileCard from '#/components/ProfileCard'
+import {Text} from '#/components/Typography'
+import {ListFooter} from '../Lists'
+import {ProgressGuideTask} from './Task'
+
+type Item =
+  | {
+      type: 'profile'
+      key: string
+      profile: AppBskyActorDefs.ProfileView
+      isSuggestion: boolean
+    }
+  | {
+      type: 'empty'
+      key: string
+      message: string
+    }
+  | {
+      type: 'placeholder'
+      key: string
+    }
+  | {
+      type: 'error'
+      key: string
+    }
+
+export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
+  const {_} = useLingui()
+  const control = Dialog.useDialogControl()
+  const {gtMobile} = useBreakpoints()
+  const {height: minHeight} = useWindowDimensions()
+
+  return (
+    <>
+      <Button
+        label={_(msg`Find people to follow`)}
+        onPress={control.open}
+        size={gtMobile ? 'small' : 'large'}
+        color="primary"
+        variant="solid">
+        <ButtonIcon icon={PersonGroupIcon} />
+        <ButtonText>
+          <Trans>Find people to follow</Trans>
+        </ButtonText>
+      </Button>
+      <Dialog.Outer control={control} nativeOptions={{minHeight}}>
+        <Dialog.Handle />
+        <DialogInner guide={guide} />
+      </Dialog.Outer>
+    </>
+  )
+}
+
+// Fine to keep this top-level.
+let lastSelectedInterest = ''
+let lastSearchText = ''
+
+function DialogInner({guide}: {guide: Follow10ProgressGuide}) {
+  const {_} = useLingui()
+  const interestsDisplayNames = useInterestsDisplayNames()
+  const {data: preferences} = usePreferencesQuery()
+  const personalizedInterests = preferences?.interests?.tags
+  const interests = Object.keys(interestsDisplayNames)
+    .sort(boostInterests(popularInterests))
+    .sort(boostInterests(personalizedInterests))
+  const [selectedInterest, setSelectedInterest] = useState(
+    () =>
+      lastSelectedInterest ||
+      (personalizedInterests && interests.includes(personalizedInterests[0])
+        ? personalizedInterests[0]
+        : interests[0]),
+  )
+  const [searchText, setSearchText] = useState(lastSearchText)
+  const moderationOpts = useModerationOpts()
+  const listRef = useRef<ListMethods>(null)
+  const inputRef = useRef<TextInput>(null)
+  const [headerHeight, setHeaderHeight] = useState(0)
+  const {currentAccount} = useSession()
+  const [suggestedAccounts, setSuggestedAccounts] = useState<
+    Map<string, AppBskyActorDefs.ProfileView[]>
+  >(() => new Map())
+
+  useEffect(() => {
+    lastSearchText = searchText
+    lastSelectedInterest = selectedInterest
+  }, [searchText, selectedInterest])
+
+  const query = searchText || selectedInterest
+  const {
+    data: searchResults,
+    isFetching,
+    error,
+    isError,
+    hasNextPage,
+    isFetchingNextPage,
+    fetchNextPage,
+  } = useActorSearchPaginated({
+    query,
+  })
+
+  const hasSearchText = !!searchText
+
+  const items = useMemo(() => {
+    const results = searchResults?.pages.flatMap(r => r.actors)
+    let _items: Item[] = []
+    const seen = new Set<string>()
+
+    if (isError) {
+      _items.push({
+        type: 'empty',
+        key: 'empty',
+        message: _(msg`We're having network issues, try again`),
+      })
+    } else if (results) {
+      // First pass: search results
+      for (const profile of results) {
+        if (profile.did === currentAccount?.did) continue
+        if (profile.viewer?.following) continue
+        // my sincere apologies to Jake Gold - your bio is too keyword-filled and
+        // your page-rank too high, so you're at the top of half the categories -sfn
+        if (
+          !hasSearchText &&
+          profile.did === 'did:plc:tpg43qhh4lw4ksiffs4nbda3' &&
+          // constrain to 'tech'
+          selectedInterest !== 'tech'
+        ) {
+          continue
+        }
+        seen.add(profile.did)
+        _items.push({
+          type: 'profile',
+          // Don't share identity across tabs or typing attempts
+          key: query + ':' + profile.did,
+          profile,
+          isSuggestion: false,
+        })
+      }
+      // Second pass: suggestions
+      _items = _items.flatMap(item => {
+        if (item.type !== 'profile') {
+          return item
+        }
+        const suggestions = suggestedAccounts.get(item.profile.did)
+        if (!suggestions) {
+          return item
+        }
+        const itemWithSuggestions = [item]
+        for (const suggested of suggestions) {
+          if (seen.has(suggested.did)) {
+            // Skip search results from previous step or already seen suggestions
+            continue
+          }
+          seen.add(suggested.did)
+          itemWithSuggestions.push({
+            type: 'profile',
+            key: suggested.did,
+            profile: suggested,
+            isSuggestion: true,
+          })
+          if (itemWithSuggestions.length === 1 + 3) {
+            break
+          }
+        }
+        return itemWithSuggestions
+      })
+    } else {
+      const placeholders: Item[] = Array(10)
+        .fill(0)
+        .map((__, i) => ({
+          type: 'placeholder',
+          key: i + '',
+        }))
+
+      _items.push(...placeholders)
+    }
+
+    return _items
+  }, [
+    _,
+    searchResults,
+    isError,
+    currentAccount?.did,
+    hasSearchText,
+    selectedInterest,
+    suggestedAccounts,
+    query,
+  ])
+
+  if (searchText && !isFetching && !items.length && !isError) {
+    items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
+  }
+
+  const renderItems = useCallback(
+    ({item, index}: {item: Item; index: number}) => {
+      switch (item.type) {
+        case 'profile': {
+          return (
+            <FollowProfileCard
+              profile={item.profile}
+              isSuggestion={item.isSuggestion}
+              moderationOpts={moderationOpts!}
+              setSuggestedAccounts={setSuggestedAccounts}
+              noBorder={index === 0}
+            />
+          )
+        }
+        case 'placeholder': {
+          return <ProfileCardSkeleton key={item.key} />
+        }
+        case 'empty': {
+          return <Empty key={item.key} message={item.message} />
+        }
+        default:
+          return null
+      }
+    },
+    [moderationOpts],
+  )
+
+  const onSelectTab = useCallback(
+    (interest: string) => {
+      setSelectedInterest(interest)
+      inputRef.current?.clear()
+      setSearchText('')
+      listRef.current?.scrollToOffset({
+        offset: 0,
+        animated: false,
+      })
+    },
+    [setSelectedInterest, setSearchText],
+  )
+
+  const listHeader = (
+    <Header
+      guide={guide}
+      inputRef={inputRef}
+      listRef={listRef}
+      searchText={searchText}
+      onSelectTab={onSelectTab}
+      setHeaderHeight={setHeaderHeight}
+      setSearchText={setSearchText}
+      interests={interests}
+      selectedInterest={selectedInterest}
+      interestsDisplayNames={interestsDisplayNames}
+    />
+  )
+
+  const onEndReached = useCallback(async () => {
+    if (isFetchingNextPage || !hasNextPage || isError) return
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more people to follow', {message: err})
+    }
+  }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
+
+  return (
+    <Dialog.InnerFlatList
+      ref={listRef}
+      data={items}
+      renderItem={renderItems}
+      ListHeaderComponent={listHeader}
+      stickyHeaderIndices={[0]}
+      keyExtractor={(item: Item) => item.key}
+      style={[
+        a.px_0,
+        web([a.py_0, {height: '100vh', maxHeight: 600}]),
+        native({height: '100%'}),
+      ]}
+      webInnerContentContainerStyle={a.py_0}
+      webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
+      keyboardDismissMode="on-drag"
+      scrollIndicatorInsets={{top: headerHeight}}
+      initialNumToRender={8}
+      maxToRenderPerBatch={8}
+      onEndReached={onEndReached}
+      itemLayoutAnimation={LinearTransition}
+      ListFooterComponent={
+        <ListFooter
+          isFetchingNextPage={isFetchingNextPage}
+          error={cleanError(error)}
+          onRetry={fetchNextPage}
+        />
+      }
+    />
+  )
+}
+
+let Header = ({
+  guide,
+  inputRef,
+  listRef,
+  searchText,
+  onSelectTab,
+  setHeaderHeight,
+  setSearchText,
+  interests,
+  selectedInterest,
+  interestsDisplayNames,
+}: {
+  guide: Follow10ProgressGuide
+  inputRef: React.RefObject<TextInput>
+  listRef: React.RefObject<ListMethods>
+  onSelectTab: (v: string) => void
+  searchText: string
+  setHeaderHeight: (v: number) => void
+  setSearchText: (v: string) => void
+  interests: string[]
+  selectedInterest: string
+  interestsDisplayNames: Record<string, string>
+}): React.ReactNode => {
+  const t = useTheme()
+  const control = Dialog.useDialogContext()
+  return (
+    <View
+      onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
+      style={[
+        a.relative,
+        web(a.pt_lg),
+        native(a.pt_4xl),
+        a.pb_xs,
+        a.border_b,
+        t.atoms.border_contrast_low,
+        t.atoms.bg,
+      ]}>
+      <HeaderTop guide={guide} />
+
+      <View style={[web(a.pt_xs), a.pb_xs]}>
+        <SearchInput
+          inputRef={inputRef}
+          defaultValue={searchText}
+          onChangeText={text => {
+            setSearchText(text)
+            listRef.current?.scrollToOffset({offset: 0, animated: false})
+          }}
+          onEscape={control.close}
+        />
+        <Tabs
+          onSelectTab={onSelectTab}
+          interests={interests}
+          selectedInterest={selectedInterest}
+          hasSearchText={!!searchText}
+          interestsDisplayNames={interestsDisplayNames}
+        />
+      </View>
+    </View>
+  )
+}
+Header = memo(Header)
+
+function HeaderTop({guide}: {guide: Follow10ProgressGuide}) {
+  const {_} = useLingui()
+  const t = useTheme()
+  const control = Dialog.useDialogContext()
+  return (
+    <View
+      style={[
+        a.px_lg,
+        a.relative,
+        a.flex_row,
+        a.justify_between,
+        a.align_center,
+      ]}>
+      <Text
+        style={[
+          a.z_10,
+          a.text_lg,
+          a.font_heavy,
+          a.leading_tight,
+          t.atoms.text_contrast_high,
+        ]}>
+        <Trans>Find people to follow</Trans>
+      </Text>
+      <View style={isWeb && {paddingRight: 36}}>
+        <ProgressGuideTask
+          current={guide.numFollows + 1}
+          total={10 + 1}
+          title={`${guide.numFollows} / 10`}
+          tabularNumsTitle
+        />
+      </View>
+      {isWeb ? (
+        <Button
+          label={_(msg`Close`)}
+          size="small"
+          shape="round"
+          variant={isWeb ? 'ghost' : 'solid'}
+          color="secondary"
+          style={[
+            a.absolute,
+            a.z_20,
+            web({right: -4}),
+            native({right: 0}),
+            native({height: 32, width: 32, borderRadius: 16}),
+          ]}
+          onPress={() => control.close()}>
+          <ButtonIcon icon={X} size="md" />
+        </Button>
+      ) : null}
+    </View>
+  )
+}
+
+let Tabs = ({
+  onSelectTab,
+  interests,
+  selectedInterest,
+  hasSearchText,
+  interestsDisplayNames,
+}: {
+  onSelectTab: (tab: string) => void
+  interests: string[]
+  selectedInterest: string
+  hasSearchText: boolean
+  interestsDisplayNames: Record<string, string>
+}): React.ReactNode => {
+  const listRef = useRef<ScrollView>(null)
+  const [scrollX, setScrollX] = useState(0)
+  const [totalWidth, setTotalWidth] = useState(0)
+  const pendingTabOffsets = useRef<{x: number; width: number}[]>([])
+  const [tabOffsets, setTabOffsets] = useState<{x: number; width: number}[]>([])
+
+  const onInitialLayout = useNonReactiveCallback(() => {
+    const index = interests.indexOf(selectedInterest)
+    scrollIntoViewIfNeeded(index)
+  })
+
+  useEffect(() => {
+    if (tabOffsets) {
+      onInitialLayout()
+    }
+  }, [tabOffsets, onInitialLayout])
+
+  function scrollIntoViewIfNeeded(index: number) {
+    const btnLayout = tabOffsets[index]
+    if (!btnLayout) return
+
+    const viewportLeftEdge = scrollX
+    const viewportRightEdge = scrollX + totalWidth
+    const shouldScrollToLeftEdge = viewportLeftEdge > btnLayout.x
+    const shouldScrollToRightEdge =
+      viewportRightEdge < btnLayout.x + btnLayout.width
+
+    if (shouldScrollToLeftEdge) {
+      listRef.current?.scrollTo({
+        x: btnLayout.x - tokens.space.lg,
+        animated: true,
+      })
+    } else if (shouldScrollToRightEdge) {
+      listRef.current?.scrollTo({
+        x: btnLayout.x - totalWidth + btnLayout.width + tokens.space.lg,
+        animated: true,
+      })
+    }
+  }
+
+  function handleSelectTab(index: number) {
+    const tab = interests[index]
+    onSelectTab(tab)
+    scrollIntoViewIfNeeded(index)
+  }
+
+  function handleTabLayout(index: number, x: number, width: number) {
+    if (!tabOffsets.length) {
+      pendingTabOffsets.current[index] = {x, width}
+      if (pendingTabOffsets.current.length === interests.length) {
+        setTabOffsets(pendingTabOffsets.current)
+      }
+    }
+  }
+
+  return (
+    <ScrollView
+      ref={listRef}
+      horizontal
+      contentContainerStyle={[a.gap_sm, a.px_lg]}
+      showsHorizontalScrollIndicator={false}
+      decelerationRate="fast"
+      snapToOffsets={
+        tabOffsets.length === interests.length
+          ? tabOffsets.map(o => o.x - tokens.space.xl)
+          : undefined
+      }
+      onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
+      scrollEventThrottle={200} // big throttle
+      onScroll={evt => setScrollX(evt.nativeEvent.contentOffset.x)}>
+      {interests.map((interest, i) => {
+        const active = interest === selectedInterest && !hasSearchText
+        return (
+          <Tab
+            key={interest}
+            onSelectTab={handleSelectTab}
+            active={active}
+            index={i}
+            interest={interest}
+            interestsDisplayName={interestsDisplayNames[interest]}
+            onLayout={handleTabLayout}
+          />
+        )
+      })}
+    </ScrollView>
+  )
+}
+Tabs = memo(Tabs)
+
+let Tab = ({
+  onSelectTab,
+  interest,
+  active,
+  index,
+  interestsDisplayName,
+  onLayout,
+}: {
+  onSelectTab: (index: number) => void
+  interest: string
+  active: boolean
+  index: number
+  interestsDisplayName: string
+  onLayout: (index: number, x: number, width: number) => void
+}): React.ReactNode => {
+  const {_} = useLingui()
+  const activeText = active ? _(msg` (active)`) : ''
+  return (
+    <View
+      key={interest}
+      onLayout={e =>
+        onLayout(index, e.nativeEvent.layout.x, e.nativeEvent.layout.width)
+      }>
+      <Button
+        label={_(msg`Search for "${interestsDisplayName}"${activeText}`)}
+        variant={active ? 'solid' : 'outline'}
+        color={active ? 'primary' : 'secondary'}
+        size="small"
+        onPress={() => onSelectTab(index)}>
+        <ButtonIcon icon={SearchIcon} />
+        <ButtonText>{interestsDisplayName}</ButtonText>
+      </Button>
+    </View>
+  )
+}
+Tab = memo(Tab)
+
+let FollowProfileCard = ({
+  profile,
+  moderationOpts,
+  isSuggestion,
+  setSuggestedAccounts,
+  noBorder,
+}: {
+  profile: AppBskyActorDefs.ProfileView
+  moderationOpts: ModerationOpts
+  isSuggestion: boolean
+  setSuggestedAccounts: (
+    updater: (
+      v: Map<string, AppBskyActorDefs.ProfileView[]>,
+    ) => Map<string, AppBskyActorDefs.ProfileView[]>,
+  ) => void
+  noBorder?: boolean
+}): React.ReactNode => {
+  const [hasFollowed, setHasFollowed] = useState(false)
+  const followupSuggestion = useSuggestedFollowsByActorQuery({
+    did: profile.did,
+    enabled: hasFollowed,
+  })
+  const candidates = followupSuggestion.data?.suggestions
+
+  useEffect(() => {
+    // TODO: Move out of effect.
+    if (hasFollowed && candidates && candidates.length > 0) {
+      setSuggestedAccounts(suggestions => {
+        const newSuggestions = new Map(suggestions)
+        newSuggestions.set(profile.did, candidates)
+        return newSuggestions
+      })
+    }
+  }, [hasFollowed, profile.did, candidates, setSuggestedAccounts])
+
+  return (
+    <LayoutAnimationConfig skipEntering={!isSuggestion}>
+      <Animated.View entering={native(ZoomInEasyDown)}>
+        <FollowProfileCardInner
+          profile={profile}
+          moderationOpts={moderationOpts}
+          onFollow={() => setHasFollowed(true)}
+          noBorder={noBorder}
+        />
+      </Animated.View>
+    </LayoutAnimationConfig>
+  )
+}
+FollowProfileCard = memo(FollowProfileCard)
+
+function FollowProfileCardInner({
+  profile,
+  moderationOpts,
+  onFollow,
+  noBorder,
+}: {
+  profile: AppBskyActorDefs.ProfileView
+  moderationOpts: ModerationOpts
+  onFollow?: () => void
+  noBorder?: boolean
+}) {
+  const control = Dialog.useDialogContext()
+  const t = useTheme()
+  return (
+    <ProfileCard.Link
+      profile={profile}
+      style={[a.flex_1]}
+      onPress={() => control.close()}>
+      {({hovered, pressed}) => (
+        <CardOuter
+          style={[
+            a.flex_1,
+            noBorder && a.border_t_0,
+            (hovered || pressed) && t.atoms.border_contrast_high,
+          ]}>
+          <ProfileCard.Outer>
+            <ProfileCard.Header>
+              <ProfileCard.Avatar
+                profile={profile}
+                moderationOpts={moderationOpts}
+              />
+              <ProfileCard.NameAndHandle
+                profile={profile}
+                moderationOpts={moderationOpts}
+              />
+              <ProfileCard.FollowButton
+                profile={profile}
+                moderationOpts={moderationOpts}
+                logContext="PostOnboardingFindFollows"
+                shape="round"
+                onPress={onFollow}
+                colorInverted
+              />
+            </ProfileCard.Header>
+            <ProfileCard.Description profile={profile} numberOfLines={2} />
+          </ProfileCard.Outer>
+        </CardOuter>
+      )}
+    </ProfileCard.Link>
+  )
+}
+
+function CardOuter({
+  children,
+  style,
+}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        a.w_full,
+        a.py_md,
+        a.px_lg,
+        a.border_t,
+        t.atoms.border_contrast_low,
+        style,
+      ]}>
+      {children}
+    </View>
+  )
+}
+
+function SearchInput({
+  onChangeText,
+  onEscape,
+  inputRef,
+  defaultValue,
+}: {
+  onChangeText: (text: string) => void
+  onEscape: () => void
+  inputRef: React.RefObject<TextInput>
+  defaultValue: string
+}) {
+  const t = useTheme()
+  const {_} = useLingui()
+  const {
+    state: hovered,
+    onIn: onMouseEnter,
+    onOut: onMouseLeave,
+  } = useInteractionState()
+  const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
+  const interacted = hovered || focused
+
+  return (
+    <View
+      {...web({
+        onMouseEnter,
+        onMouseLeave,
+      })}
+      style={[a.flex_row, a.align_center, a.gap_sm, a.px_lg, a.py_xs]}>
+      <SearchIcon
+        size="md"
+        fill={interacted ? t.palette.primary_500 : t.palette.contrast_300}
+      />
+
+      <TextInput
+        ref={inputRef}
+        placeholder={_(msg`Search`)}
+        defaultValue={defaultValue}
+        onChangeText={onChangeText}
+        onFocus={onFocus}
+        onBlur={onBlur}
+        style={[a.flex_1, a.py_md, a.text_md, t.atoms.text]}
+        placeholderTextColor={t.palette.contrast_500}
+        keyboardAppearance={t.name === 'light' ? 'light' : 'dark'}
+        returnKeyType="search"
+        clearButtonMode="while-editing"
+        maxLength={50}
+        onKeyPress={({nativeEvent}) => {
+          if (nativeEvent.key === 'Escape') {
+            onEscape()
+          }
+        }}
+        autoCorrect={false}
+        autoComplete="off"
+        autoCapitalize="none"
+        accessibilityLabel={_(msg`Search profiles`)}
+        accessibilityHint={_(msg`Search profiles`)}
+      />
+    </View>
+  )
+}
+
+function ProfileCardSkeleton() {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.flex_1,
+        a.py_md,
+        a.px_lg,
+        a.gap_md,
+        a.align_center,
+        a.flex_row,
+      ]}>
+      <View
+        style={[
+          a.rounded_full,
+          {width: 42, height: 42},
+          t.atoms.bg_contrast_25,
+        ]}
+      />
+
+      <View style={[a.flex_1, a.gap_sm]}>
+        <View
+          style={[
+            a.rounded_xs,
+            {width: 80, height: 14},
+            t.atoms.bg_contrast_25,
+          ]}
+        />
+        <View
+          style={[
+            a.rounded_xs,
+            {width: 120, height: 10},
+            t.atoms.bg_contrast_25,
+          ]}
+        />
+      </View>
+    </View>
+  )
+}
+
+function Empty({message}: {message: string}) {
+  const t = useTheme()
+  return (
+    <View style={[a.p_lg, a.py_xl, a.align_center, a.gap_md]}>
+      <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_high]}>
+        {message}
+      </Text>
+
+      <Text style={[a.text_xs, t.atoms.text_contrast_low]}>(╯°□°)╯︵ ┻━┻</Text>
+    </View>
+  )
+}
+
+function boostInterests(boosts?: string[]) {
+  return (_a: string, _b: string) => {
+    const indexA = boosts?.indexOf(_a) ?? -1
+    const indexB = boosts?.indexOf(_b) ?? -1
+    const rankA = indexA === -1 ? Infinity : indexA
+    const rankB = indexB === -1 ? Infinity : indexB
+    return rankA - rankB
+  }
+}
diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx
index 299d1e69f..bbc5a0177 100644
--- a/src/components/ProgressGuide/List.tsx
+++ b/src/components/ProgressGuide/List.tsx
@@ -10,12 +10,15 @@ import {atoms as a, useTheme} from '#/alf'
 import {Button, ButtonIcon} from '#/components/Button'
 import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
 import {Text} from '#/components/Typography'
+import {FollowDialog} from './FollowDialog'
 import {ProgressGuideTask} from './Task'
 
 export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
   const t = useTheme()
   const {_} = useLingui()
-  const guide = useProgressGuide('like-10-and-follow-7')
+  const followProgressGuide = useProgressGuide('follow-10')
+  const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
+  const guide = followProgressGuide || followAndLikeProgressGuide
   const {endProgressGuide} = useProgressGuideControls()
 
   if (guide) {
@@ -41,18 +44,33 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
             <ButtonIcon icon={Times} size="sm" />
           </Button>
         </View>
-        <ProgressGuideTask
-          current={guide.numLikes + 1}
-          total={10 + 1}
-          title={_(msg`Like 10 posts`)}
-          subtitle={_(msg`Teach our algorithm what you like`)}
-        />
-        <ProgressGuideTask
-          current={guide.numFollows + 1}
-          total={7 + 1}
-          title={_(msg`Follow 7 accounts`)}
-          subtitle={_(msg`Bluesky is better with friends!`)}
-        />
+        {guide.guide === 'follow-10' && (
+          <>
+            <ProgressGuideTask
+              current={guide.numFollows + 1}
+              total={10 + 1}
+              title={_(msg`Follow 10 accounts`)}
+              subtitle={_(msg`Bluesky is better with friends!`)}
+            />
+            <FollowDialog guide={guide} />
+          </>
+        )}
+        {guide.guide === 'like-10-and-follow-7' && (
+          <>
+            <ProgressGuideTask
+              current={guide.numLikes + 1}
+              total={10 + 1}
+              title={_(msg`Like 10 posts`)}
+              subtitle={_(msg`Teach our algorithm what you like`)}
+            />
+            <ProgressGuideTask
+              current={guide.numFollows + 1}
+              total={7 + 1}
+              title={_(msg`Follow 7 accounts`)}
+              subtitle={_(msg`Bluesky is better with friends!`)}
+            />
+          </>
+        )}
       </View>
     )
   }
diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx
index 973ee1ac7..b9ba3fd9a 100644
--- a/src/components/ProgressGuide/Task.tsx
+++ b/src/components/ProgressGuide/Task.tsx
@@ -10,11 +10,13 @@ export function ProgressGuideTask({
   total,
   title,
   subtitle,
+  tabularNumsTitle,
 }: {
   current: number
   total: number
   title: string
   subtitle?: string
+  tabularNumsTitle?: boolean
 }) {
   const t = useTheme()
 
@@ -33,8 +35,16 @@ export function ProgressGuideTask({
         />
       )}
 
-      <View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
-        <Text style={[a.text_sm, a.font_bold, a.leading_tight]}>{title}</Text>
+      <View style={[a.flex_col, a.gap_2xs, subtitle && {marginTop: -2}]}>
+        <Text
+          style={[
+            a.text_sm,
+            a.font_bold,
+            a.leading_tight,
+            tabularNumsTitle && {fontVariant: ['tabular-nums']},
+          ]}>
+          {title}
+        </Text>
         {subtitle && (
           <Text
             style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
diff --git a/src/components/dms/dialogs/SearchablePeopleList.tsx b/src/components/dms/dialogs/SearchablePeopleList.tsx
index 0946d2a27..50090cbcb 100644
--- a/src/components/dms/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dms/dialogs/SearchablePeopleList.tsx
@@ -63,6 +63,7 @@ export function SearchablePeopleList({
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const control = Dialog.useDialogContext()
+  const [headerHeight, setHeaderHeight] = useState(0)
   const listRef = useRef<ListMethods>(null)
   const {currentAccount} = useSession()
   const inputRef = useRef<TextInput>(null)
@@ -237,6 +238,7 @@ export function SearchablePeopleList({
   const listHeader = useMemo(() => {
     return (
       <View
+        onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
         style={[
           a.relative,
           web(a.pt_lg),
@@ -315,6 +317,7 @@ export function SearchablePeopleList({
       ]}
       webInnerContentContainerStyle={a.py_0}
       webInnerStyle={[a.py_0, {maxWidth: 500, minWidth: 200}]}
+      scrollIndicatorInsets={{top: headerHeight}}
       keyboardDismissMode="on-drag"
     />
   )
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index e6c9c5d13..f1dfb0a94 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -162,6 +162,7 @@ export type LogEvents = {
       | 'StarterPackProfilesList'
       | 'FeedInterstitial'
       | 'ProfileHeaderSuggestedFollows'
+      | 'PostOnboardingFindFollows'
   }
   'profile:unfollow': {
     logContext:
@@ -177,6 +178,7 @@ export type LogEvents = {
       | 'StarterPackProfilesList'
       | 'FeedInterstitial'
       | 'ProfileHeaderSuggestedFollows'
+      | 'PostOnboardingFindFollows'
   }
   'chat:create': {
     logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog'
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index 6876f18c5..a6c249254 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -1,3 +1,6 @@
 export type Gate =
   // Keep this alphabetic please.
-  'debug_show_feedcontext' | 'debug_subscriptions' | 'remove_show_latest_button'
+  | 'debug_show_feedcontext'
+  | 'debug_subscriptions'
+  | 'new_postonboarding'
+  | 'remove_show_latest_button'
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 0d8971b6f..fc0ea6a24 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -14,7 +14,7 @@ import {
   TIMELINE_SAVED_FEED,
 } from '#/lib/constants'
 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
-import {logEvent} from '#/lib/statsig/statsig'
+import {logEvent, useGate} from '#/lib/statsig/statsig'
 import {logger} from '#/logger'
 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
 import {getAllListMembers} from '#/state/queries/list-members'
@@ -57,6 +57,7 @@ export function StepFinished() {
   const setActiveStarterPack = useSetActiveStarterPack()
   const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
   const {startProgressGuide} = useProgressGuideControls()
+  const gate = useGate()
 
   const finishOnboarding = React.useCallback(async () => {
     setSaving(true)
@@ -190,7 +191,9 @@ export function StepFinished() {
     setSaving(false)
     setActiveStarterPack(undefined)
     setHasCheckedForStarterPack(true)
-    startProgressGuide('like-10-and-follow-7')
+    startProgressGuide(
+      gate('new_postonboarding') ? 'follow-10' : 'like-10-and-follow-7',
+    )
     dispatch({type: 'finish'})
     onboardDispatch({type: 'finish'})
     logEvent('onboarding:finished:nextPressed', {
@@ -221,6 +224,7 @@ export function StepFinished() {
     setActiveStarterPack,
     setHasCheckedForStarterPack,
     startProgressGuide,
+    gate,
   ])
 
   return (
diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts
index 70fa69640..20d3ef217 100644
--- a/src/screens/Onboarding/state.ts
+++ b/src/screens/Onboarding/state.ts
@@ -72,6 +72,19 @@ export type ApiResponseMap = {
   }
 }
 
+// most popular selected interests
+export const popularInterests = [
+  'art',
+  'gaming',
+  'sports',
+  'comics',
+  'music',
+  'politics',
+  'photography',
+  'science',
+  'news',
+]
+
 export function useInterestsDisplayNames() {
   const {_} = useLingui()
 
diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts
index 479fc1a9f..6d6c46e04 100644
--- a/src/state/queries/actor-search.ts
+++ b/src/state/queries/actor-search.ts
@@ -1,6 +1,7 @@
 import {AppBskyActorDefs, AppBskyActorSearchActors} from '@atproto/api'
 import {
   InfiniteData,
+  keepPreviousData,
   QueryClient,
   QueryKey,
   useInfiniteQuery,
@@ -13,10 +14,8 @@ import {useAgent} from '#/state/session'
 const RQKEY_ROOT = 'actor-search'
 export const RQKEY = (query: string) => [RQKEY_ROOT, query]
 
-export const RQKEY_PAGINATED = (query: string) => [
-  `${RQKEY_ROOT}_paginated`,
-  query,
-]
+const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
+export const RQKEY_PAGINATED = (query: string) => [RQKEY_ROOT_PAGINATED, query]
 
 export function useActorSearch({
   query,
@@ -42,9 +41,11 @@ export function useActorSearch({
 export function useActorSearchPaginated({
   query,
   enabled,
+  maintainData,
 }: {
   query: string
   enabled?: boolean
+  maintainData?: boolean
 }) {
   const agent = useAgent()
   return useInfiniteQuery<
@@ -67,6 +68,7 @@ export function useActorSearchPaginated({
     enabled: enabled && !!query,
     initialPageParam: undefined,
     getNextPageParam: lastPage => lastPage.cursor,
+    placeholderData: maintainData ? keepPreviousData : undefined,
   })
 }
 
@@ -89,4 +91,20 @@ export function* findAllProfilesInQueryData(
       }
     }
   }
+
+  const queryDatasPaginated = queryClient.getQueriesData<
+    InfiniteData<AppBskyActorSearchActors.OutputSchema>
+  >({
+    queryKey: [RQKEY_ROOT_PAGINATED],
+  })
+  for (const [_queryKey, queryData] of queryDatasPaginated) {
+    if (!queryData) {
+      continue
+    }
+    for (const actor of queryData.pages.flatMap(page => page.actors)) {
+      if (actor.did === did) {
+        yield actor
+      }
+    }
+  }
 }
diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts
index 07e16946e..22033c0a8 100644
--- a/src/state/queries/suggested-follows.ts
+++ b/src/state/queries/suggested-follows.ts
@@ -103,7 +103,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
   })
 }
 
-export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
+export function useSuggestedFollowsByActorQuery({
+  did,
+  enabled,
+}: {
+  did: string
+  enabled?: boolean
+}) {
   const agent = useAgent()
   return useQuery({
     queryKey: suggestedFollowsByActorQueryKey(did),
@@ -116,6 +122,7 @@ export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
         : res.data.suggestions.filter(profile => !profile.viewer?.following)
       return {suggestions}
     },
+    enabled,
   })
 }
 
diff --git a/src/state/shell/progress-guide.tsx b/src/state/shell/progress-guide.tsx
index d64e9984f..af3d60ebb 100644
--- a/src/state/shell/progress-guide.tsx
+++ b/src/state/shell/progress-guide.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, {useMemo} from 'react'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
@@ -16,20 +16,32 @@ export enum ProgressGuideAction {
   Follow = 'follow',
 }
 
-type ProgressGuideName = 'like-10-and-follow-7'
+type ProgressGuideName = 'like-10-and-follow-7' | 'follow-10'
 
+/**
+ * Progress Guides that extend this interface must specify their name in the `guide` field, so it can be used as a discriminated union
+ */
 interface BaseProgressGuide {
-  guide: string
+  guide: ProgressGuideName
   isComplete: boolean
   [key: string]: any
 }
 
-interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
+export interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
+  guide: 'like-10-and-follow-7'
   numLikes: number
   numFollows: number
 }
 
-type ProgressGuide = Like10AndFollow7ProgressGuide | undefined
+export interface Follow10ProgressGuide extends BaseProgressGuide {
+  guide: 'follow-10'
+  numFollows: number
+}
+
+export type ProgressGuide =
+  | Like10AndFollow7ProgressGuide
+  | Follow10ProgressGuide
+  | undefined
 
 const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
 
@@ -61,15 +73,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const {mutateAsync, variables, isPending} =
     useSetActiveProgressGuideMutation()
 
-  const activeProgressGuide = (
-    isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
-  ) as ProgressGuide
+  const activeProgressGuide = useMemo(() => {
+    const rawProgressGuide = (
+      isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
+    ) as ProgressGuide
+
+    if (!rawProgressGuide) return undefined
+
+    // ensure the unspecced attributes have the correct types
+    // clone then mutate
+    const {...maybeWronglyTypedProgressGuide} = rawProgressGuide
+    if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') {
+      maybeWronglyTypedProgressGuide.numLikes =
+        Number(maybeWronglyTypedProgressGuide.numLikes) || 0
+      maybeWronglyTypedProgressGuide.numFollows =
+        Number(maybeWronglyTypedProgressGuide.numFollows) || 0
+    } else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') {
+      maybeWronglyTypedProgressGuide.numFollows =
+        Number(maybeWronglyTypedProgressGuide.numFollows) || 0
+    }
 
-  // ensure the unspecced attributes have the correct types
-  if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
-    activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
-    activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
-  }
+    return maybeWronglyTypedProgressGuide
+  }, [isPending, variables, preferences])
 
   const [localGuideState, setLocalGuideState] =
     React.useState<ProgressGuide>(undefined)
@@ -82,7 +107,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
   const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
   const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
-  const guideCompleteToastRef = React.useRef<ProgressGuideToastRef | null>(null)
+
+  const fifthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
+  const tenthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
 
   const controls = React.useMemo(() => {
     return {
@@ -93,7 +120,15 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
             numLikes: 0,
             numFollows: 0,
             isComplete: false,
-          }
+          } satisfies ProgressGuide
+          setLocalGuideState(guideObj)
+          mutateAsync(guideObj)
+        } else if (guide === 'follow-10') {
+          const guideObj = {
+            guide: 'follow-10',
+            numFollows: 0,
+            isComplete: false,
+          } satisfies ProgressGuide
           setLocalGuideState(guideObj)
           mutateAsync(guideObj)
         }
@@ -137,6 +172,26 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
               isComplete: true,
             }
           }
+        } else if (guide?.guide === 'follow-10') {
+          if (action === ProgressGuideAction.Follow) {
+            guide = {
+              ...guide,
+              numFollows: (Number(guide.numFollows) || 0) + count,
+            }
+
+            if (guide.numFollows === 5) {
+              fifthFollowToastRef.current?.open()
+            }
+            if (guide.numFollows === 10) {
+              tenthFollowToastRef.current?.open()
+            }
+          }
+          if (Number(guide.numFollows) >= 10) {
+            guide = {
+              ...guide,
+              isComplete: true,
+            }
+          }
         }
 
         setLocalGuideState(guide)
@@ -167,9 +222,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
               subtitle={_(msg`The Discover feed now knows what you like`)}
             />
             <ProgressGuideToast
-              ref={guideCompleteToastRef}
-              title={_(msg`Algorithm training complete!`)}
-              subtitle={_(msg`The Discover feed now knows what you like`)}
+              ref={fifthFollowToastRef}
+              title={_(msg`Half way there!`)}
+              subtitle={_(msg`Follow 10 accounts`)}
+            />
+            <ProgressGuideToast
+              ref={tenthFollowToastRef}
+              title={_(msg`Task complete - 10 follows!`)}
+              subtitle={_(msg`You've found some people to follow`)}
             />
           </>
         )}
diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx
index 55d7ba053..10eb47d0a 100644
--- a/src/view/com/posts/PostFeed.tsx
+++ b/src/view/com/posts/PostFeed.tsx
@@ -253,9 +253,11 @@ let PostFeed = ({
     }
   }, [pollInterval])
 
-  const progressGuide = useProgressGuide('like-10-and-follow-7')
+  const followProgressGuide = useProgressGuide('follow-10')
+  const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
   const {isDesktop} = useWebMediaQueries()
-  const showProgressIntersitial = progressGuide && !isDesktop
+  const showProgressIntersitial =
+    (followProgressGuide || followAndLikeProgressGuide) && !isDesktop
 
   const feedItems: FeedRow[] = React.useMemo(() => {
     let feedKind: 'following' | 'discover' | 'profile' | undefined
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index 62c91cec6..5084af612 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -155,7 +155,11 @@ let List = React.forwardRef<ListMethods, ListProps>(
         automaticallyAdjustsScrollIndicatorInsets={
           automaticallyAdjustsScrollIndicatorInsets
         }
-        scrollIndicatorInsets={{top: headerOffset, right: 1}}
+        scrollIndicatorInsets={{
+          top: headerOffset,
+          right: 1,
+          ...props.scrollIndicatorInsets,
+        }}
         contentOffset={contentOffset}
         refreshControl={refreshControl}
         onScroll={scrollHandler}
diff --git a/src/view/com/util/Toast.tsx b/src/view/com/util/Toast.tsx
index 7dc6837e2..c60288674 100644
--- a/src/view/com/util/Toast.tsx
+++ b/src/view/com/util/Toast.tsx
@@ -196,7 +196,9 @@ function Toast({
                   />
                 </View>
                 <View style={[a.h_full, a.justify_center, a.flex_1]}>
-                  <Text style={a.text_md}>{message}</Text>
+                  <Text style={a.text_md} emoji>
+                    {message}
+                  </Text>
                 </View>
               </View>
             </GestureDetector>