about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-09-08 00:38:57 +0100
committerGitHub <noreply@github.com>2023-09-07 16:38:57 -0700
commita5b89dffa6713bb06c1c572bbdc00517cf5e9bc5 (patch)
treeac8aa2deec7cc24d9a3553bab0c48335d2e8677c /src
parent00595591c46db6ebfa9a8ee404f275b43493f7e0 (diff)
downloadvoidsky-a5b89dffa6713bb06c1c572bbdc00517cf5e9bc5.tar.zst
Add ESLint React plugin (#1412)
* Add eslint-plugin-react

* Enable display name rule
Diffstat (limited to 'src')
-rw-r--r--src/view/com/composer/text-input/TextInput.tsx330
-rw-r--r--src/view/com/composer/text-input/TextInput.web.tsx219
-rw-r--r--src/view/com/composer/text-input/web/Autocomplete.tsx2
-rw-r--r--src/view/com/pager/Pager.tsx4
-rw-r--r--src/view/com/pager/Pager.web.tsx88
-rw-r--r--src/view/com/search/Suggestions.tsx328
-rw-r--r--src/view/com/util/PressableWithHover.tsx56
-rw-r--r--src/view/com/util/ViewSelector.tsx170
-rw-r--r--src/view/com/util/Views.web.tsx4
-rw-r--r--src/view/com/util/anim/TriggerableAnimated.tsx2
-rw-r--r--src/view/com/util/layouts/withBreakpoints.tsx13
11 files changed, 600 insertions, 616 deletions
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 32fdb4aa2..c5d094ea5 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -51,181 +51,179 @@ interface Selection {
   end: number
 }
 
-export const TextInput = forwardRef(
-  (
-    {
-      richtext,
-      placeholder,
-      suggestedLinks,
-      autocompleteView,
-      setRichText,
-      onPhotoPasted,
-      onSuggestedLinksChanged,
-      onError,
-      ...props
-    }: TextInputProps,
-    ref,
-  ) => {
-    const pal = usePalette('default')
-    const textInput = useRef<PasteInputRef>(null)
-    const textInputSelection = useRef<Selection>({start: 0, end: 0})
-    const theme = useTheme()
-
-    React.useImperativeHandle(ref, () => ({
-      focus: () => textInput.current?.focus(),
-      blur: () => {
-        textInput.current?.blur()
-      },
-    }))
-
-    const onChangeText = useCallback(
-      (newText: string) => {
-        /*
-         * This is a hack to bump the rendering of our styled
-         * `textDecorated` to _after_ whatever processing is happening
-         * within the `PasteInput` library. Without this, the elements in
-         * `textDecorated` are not correctly painted to screen.
-         *
-         * NB: we tried a `0` timeout as well, but only positive values worked.
-         *
-         * @see https://github.com/bluesky-social/social-app/issues/929
-         */
-        setTimeout(async () => {
-          const newRt = new RichText({text: newText})
-          newRt.detectFacetsWithoutResolution()
-          setRichText(newRt)
-
-          const prefix = getMentionAt(
-            newText,
-            textInputSelection.current?.start || 0,
-          )
-          if (prefix) {
-            autocompleteView.setActive(true)
-            autocompleteView.setPrefix(prefix.value)
-          } else {
-            autocompleteView.setActive(false)
-          }
+export const TextInput = forwardRef(function TextInputImpl(
+  {
+    richtext,
+    placeholder,
+    suggestedLinks,
+    autocompleteView,
+    setRichText,
+    onPhotoPasted,
+    onSuggestedLinksChanged,
+    onError,
+    ...props
+  }: TextInputProps,
+  ref,
+) {
+  const pal = usePalette('default')
+  const textInput = useRef<PasteInputRef>(null)
+  const textInputSelection = useRef<Selection>({start: 0, end: 0})
+  const theme = useTheme()
+
+  React.useImperativeHandle(ref, () => ({
+    focus: () => textInput.current?.focus(),
+    blur: () => {
+      textInput.current?.blur()
+    },
+  }))
+
+  const onChangeText = useCallback(
+    (newText: string) => {
+      /*
+       * This is a hack to bump the rendering of our styled
+       * `textDecorated` to _after_ whatever processing is happening
+       * within the `PasteInput` library. Without this, the elements in
+       * `textDecorated` are not correctly painted to screen.
+       *
+       * NB: we tried a `0` timeout as well, but only positive values worked.
+       *
+       * @see https://github.com/bluesky-social/social-app/issues/929
+       */
+      setTimeout(async () => {
+        const newRt = new RichText({text: newText})
+        newRt.detectFacetsWithoutResolution()
+        setRichText(newRt)
+
+        const prefix = getMentionAt(
+          newText,
+          textInputSelection.current?.start || 0,
+        )
+        if (prefix) {
+          autocompleteView.setActive(true)
+          autocompleteView.setPrefix(prefix.value)
+        } else {
+          autocompleteView.setActive(false)
+        }
 
-          const set: Set<string> = new Set()
-
-          if (newRt.facets) {
-            for (const facet of newRt.facets) {
-              for (const feature of facet.features) {
-                if (AppBskyRichtextFacet.isLink(feature)) {
-                  if (isUriImage(feature.uri)) {
-                    const res = await downloadAndResize({
-                      uri: feature.uri,
-                      width: POST_IMG_MAX.width,
-                      height: POST_IMG_MAX.height,
-                      mode: 'contain',
-                      maxSize: POST_IMG_MAX.size,
-                      timeout: 15e3,
-                    })
-
-                    if (res !== undefined) {
-                      onPhotoPasted(res.path)
-                    }
-                  } else {
-                    set.add(feature.uri)
+        const set: Set<string> = new Set()
+
+        if (newRt.facets) {
+          for (const facet of newRt.facets) {
+            for (const feature of facet.features) {
+              if (AppBskyRichtextFacet.isLink(feature)) {
+                if (isUriImage(feature.uri)) {
+                  const res = await downloadAndResize({
+                    uri: feature.uri,
+                    width: POST_IMG_MAX.width,
+                    height: POST_IMG_MAX.height,
+                    mode: 'contain',
+                    maxSize: POST_IMG_MAX.size,
+                    timeout: 15e3,
+                  })
+
+                  if (res !== undefined) {
+                    onPhotoPasted(res.path)
                   }
+                } else {
+                  set.add(feature.uri)
                 }
               }
             }
           }
-
-          if (!isEqual(set, suggestedLinks)) {
-            onSuggestedLinksChanged(set)
-          }
-        }, 1)
-      },
-      [
-        setRichText,
-        autocompleteView,
-        suggestedLinks,
-        onSuggestedLinksChanged,
-        onPhotoPasted,
-      ],
-    )
-
-    const onPaste = useCallback(
-      async (err: string | undefined, files: PastedFile[]) => {
-        if (err) {
-          return onError(cleanError(err))
         }
 
-        const uris = files.map(f => f.uri)
-        const uri = uris.find(isUriImage)
-
-        if (uri) {
-          onPhotoPasted(uri)
+        if (!isEqual(set, suggestedLinks)) {
+          onSuggestedLinksChanged(set)
         }
-      },
-      [onError, onPhotoPasted],
-    )
-
-    const onSelectionChange = useCallback(
-      (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
-        // NOTE we track the input selection using a ref to avoid excessive renders -prf
-        textInputSelection.current = evt.nativeEvent.selection
-      },
-      [textInputSelection],
-    )
-
-    const onSelectAutocompleteItem = useCallback(
-      (item: string) => {
-        onChangeText(
-          insertMentionAt(
-            richtext.text,
-            textInputSelection.current?.start || 0,
-            item,
-          ),
-        )
-        autocompleteView.setActive(false)
-      },
-      [onChangeText, richtext, autocompleteView],
-    )
-
-    const textDecorated = useMemo(() => {
-      let i = 0
-
-      return Array.from(richtext.segments()).map(segment => (
-        <Text
-          key={i++}
-          style={[
-            !segment.facet ? pal.text : pal.link,
-            styles.textInputFormatting,
-          ]}>
-          {segment.text}
-        </Text>
-      ))
-    }, [richtext, pal.link, pal.text])
-
-    return (
-      <View style={styles.container}>
-        <PasteInput
-          testID="composerTextInput"
-          ref={textInput}
-          onChangeText={onChangeText}
-          onPaste={onPaste}
-          onSelectionChange={onSelectionChange}
-          placeholder={placeholder}
-          placeholderTextColor={pal.colors.textLight}
-          keyboardAppearance={theme.colorScheme}
-          autoFocus={true}
-          allowFontScaling
-          multiline
-          style={[pal.text, styles.textInput, styles.textInputFormatting]}
-          {...props}>
-          {textDecorated}
-        </PasteInput>
-        <Autocomplete
-          view={autocompleteView}
-          onSelect={onSelectAutocompleteItem}
-        />
-      </View>
-    )
-  },
-)
+      }, 1)
+    },
+    [
+      setRichText,
+      autocompleteView,
+      suggestedLinks,
+      onSuggestedLinksChanged,
+      onPhotoPasted,
+    ],
+  )
+
+  const onPaste = useCallback(
+    async (err: string | undefined, files: PastedFile[]) => {
+      if (err) {
+        return onError(cleanError(err))
+      }
+
+      const uris = files.map(f => f.uri)
+      const uri = uris.find(isUriImage)
+
+      if (uri) {
+        onPhotoPasted(uri)
+      }
+    },
+    [onError, onPhotoPasted],
+  )
+
+  const onSelectionChange = useCallback(
+    (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
+      // NOTE we track the input selection using a ref to avoid excessive renders -prf
+      textInputSelection.current = evt.nativeEvent.selection
+    },
+    [textInputSelection],
+  )
+
+  const onSelectAutocompleteItem = useCallback(
+    (item: string) => {
+      onChangeText(
+        insertMentionAt(
+          richtext.text,
+          textInputSelection.current?.start || 0,
+          item,
+        ),
+      )
+      autocompleteView.setActive(false)
+    },
+    [onChangeText, richtext, autocompleteView],
+  )
+
+  const textDecorated = useMemo(() => {
+    let i = 0
+
+    return Array.from(richtext.segments()).map(segment => (
+      <Text
+        key={i++}
+        style={[
+          !segment.facet ? pal.text : pal.link,
+          styles.textInputFormatting,
+        ]}>
+        {segment.text}
+      </Text>
+    ))
+  }, [richtext, pal.link, pal.text])
+
+  return (
+    <View style={styles.container}>
+      <PasteInput
+        testID="composerTextInput"
+        ref={textInput}
+        onChangeText={onChangeText}
+        onPaste={onPaste}
+        onSelectionChange={onSelectionChange}
+        placeholder={placeholder}
+        placeholderTextColor={pal.colors.textLight}
+        keyboardAppearance={theme.colorScheme}
+        autoFocus={true}
+        allowFontScaling
+        multiline
+        style={[pal.text, styles.textInput, styles.textInputFormatting]}
+        {...props}>
+        {textDecorated}
+      </PasteInput>
+      <Autocomplete
+        view={autocompleteView}
+        onSelect={onSelectAutocompleteItem}
+      />
+    </View>
+  )
+})
 
 const styles = StyleSheet.create({
   container: {
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index 77f634930..e90298817 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -37,135 +37,130 @@ interface TextInputProps {
 
 export const textInputWebEmitter = new EventEmitter()
 
-export const TextInput = React.forwardRef(
-  (
-    {
-      richtext,
-      placeholder,
-      suggestedLinks,
-      autocompleteView,
-      setRichText,
-      onPhotoPasted,
-      onPressPublish,
-      onSuggestedLinksChanged,
-    }: // onError, TODO
-    TextInputProps,
-    ref,
-  ) => {
-    const modeClass = useColorSchemeStyle(
-      'ProseMirror-light',
-      'ProseMirror-dark',
-    )
+export const TextInput = React.forwardRef(function TextInputImpl(
+  {
+    richtext,
+    placeholder,
+    suggestedLinks,
+    autocompleteView,
+    setRichText,
+    onPhotoPasted,
+    onPressPublish,
+    onSuggestedLinksChanged,
+  }: // onError, TODO
+  TextInputProps,
+  ref,
+) {
+  const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark')
 
-    React.useEffect(() => {
-      textInputWebEmitter.addListener('publish', onPressPublish)
-      return () => {
-        textInputWebEmitter.removeListener('publish', onPressPublish)
-      }
-    }, [onPressPublish])
-    React.useEffect(() => {
-      textInputWebEmitter.addListener('photo-pasted', onPhotoPasted)
-      return () => {
-        textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted)
-      }
-    }, [onPhotoPasted])
+  React.useEffect(() => {
+    textInputWebEmitter.addListener('publish', onPressPublish)
+    return () => {
+      textInputWebEmitter.removeListener('publish', onPressPublish)
+    }
+  }, [onPressPublish])
+  React.useEffect(() => {
+    textInputWebEmitter.addListener('photo-pasted', onPhotoPasted)
+    return () => {
+      textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted)
+    }
+  }, [onPhotoPasted])
 
-    const editor = useEditor(
-      {
-        extensions: [
-          Document,
-          LinkDecorator,
-          Mention.configure({
-            HTMLAttributes: {
-              class: 'mention',
-            },
-            suggestion: createSuggestion({autocompleteView}),
-          }),
-          Paragraph,
-          Placeholder.configure({
-            placeholder,
-          }),
-          Text,
-          History,
-          Hardbreak,
-        ],
-        editorProps: {
-          attributes: {
-            class: modeClass,
+  const editor = useEditor(
+    {
+      extensions: [
+        Document,
+        LinkDecorator,
+        Mention.configure({
+          HTMLAttributes: {
+            class: 'mention',
           },
-          handlePaste: (_, event) => {
-            const items = event.clipboardData?.items
+          suggestion: createSuggestion({autocompleteView}),
+        }),
+        Paragraph,
+        Placeholder.configure({
+          placeholder,
+        }),
+        Text,
+        History,
+        Hardbreak,
+      ],
+      editorProps: {
+        attributes: {
+          class: modeClass,
+        },
+        handlePaste: (_, event) => {
+          const items = event.clipboardData?.items
 
-            if (items === undefined) {
-              return
-            }
+          if (items === undefined) {
+            return
+          }
 
-            getImageFromUri(items, (uri: string) => {
-              textInputWebEmitter.emit('photo-pasted', uri)
-            })
-          },
-          handleKeyDown: (_, event) => {
-            if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
-              textInputWebEmitter.emit('publish')
-            }
-          },
+          getImageFromUri(items, (uri: string) => {
+            textInputWebEmitter.emit('photo-pasted', uri)
+          })
         },
-        content: textToEditorJson(richtext.text.toString()),
-        autofocus: 'end',
-        editable: true,
-        injectCSS: true,
-        onUpdate({editor: editorProp}) {
-          const json = editorProp.getJSON()
+        handleKeyDown: (_, event) => {
+          if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
+            textInputWebEmitter.emit('publish')
+          }
+        },
+      },
+      content: textToEditorJson(richtext.text.toString()),
+      autofocus: 'end',
+      editable: true,
+      injectCSS: true,
+      onUpdate({editor: editorProp}) {
+        const json = editorProp.getJSON()
 
-          const newRt = new RichText({text: editorJsonToText(json).trim()})
-          newRt.detectFacetsWithoutResolution()
-          setRichText(newRt)
+        const newRt = new RichText({text: editorJsonToText(json).trim()})
+        newRt.detectFacetsWithoutResolution()
+        setRichText(newRt)
 
-          const set: Set<string> = new Set()
+        const set: Set<string> = new Set()
 
-          if (newRt.facets) {
-            for (const facet of newRt.facets) {
-              for (const feature of facet.features) {
-                if (AppBskyRichtextFacet.isLink(feature)) {
-                  set.add(feature.uri)
-                }
+        if (newRt.facets) {
+          for (const facet of newRt.facets) {
+            for (const feature of facet.features) {
+              if (AppBskyRichtextFacet.isLink(feature)) {
+                set.add(feature.uri)
               }
             }
           }
+        }
 
-          if (!isEqual(set, suggestedLinks)) {
-            onSuggestedLinksChanged(set)
-          }
-        },
+        if (!isEqual(set, suggestedLinks)) {
+          onSuggestedLinksChanged(set)
+        }
       },
-      [modeClass],
-    )
+    },
+    [modeClass],
+  )
 
-    const onEmojiInserted = React.useCallback(
-      (emoji: Emoji) => {
-        editor?.chain().focus().insertContent(emoji.native).run()
-      },
-      [editor],
-    )
-    React.useEffect(() => {
-      textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
-      return () => {
-        textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
-      }
-    }, [onEmojiInserted])
+  const onEmojiInserted = React.useCallback(
+    (emoji: Emoji) => {
+      editor?.chain().focus().insertContent(emoji.native).run()
+    },
+    [editor],
+  )
+  React.useEffect(() => {
+    textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
+    return () => {
+      textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
+    }
+  }, [onEmojiInserted])
 
-    React.useImperativeHandle(ref, () => ({
-      focus: () => {}, // TODO
-      blur: () => {}, // TODO
-    }))
+  React.useImperativeHandle(ref, () => ({
+    focus: () => {}, // TODO
+    blur: () => {}, // TODO
+  }))
 
-    return (
-      <View style={styles.container}>
-        <EditorContent editor={editor} />
-      </View>
-    )
-  },
-)
+  return (
+    <View style={styles.container}>
+      <EditorContent editor={editor} />
+    </View>
+  )
+})
 
 function editorJsonToText(json: JSONContent): string {
   let text = ''
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index 87820b97b..bbed26d48 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -94,7 +94,7 @@ export function createSuggestion({
 }
 
 const MentionList = forwardRef<MentionListRef, SuggestionProps>(
-  (props: SuggestionProps, ref) => {
+  function MentionListImpl(props: SuggestionProps, ref) {
     const [selectedIndex, setSelectedIndex] = useState(0)
     const pal = usePalette('default')
     const {getGraphemeString} = useGrapheme()
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index ad271da33..39ba29bda 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -24,7 +24,7 @@ interface Props {
   testID?: string
 }
 export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
-  (
+  function PagerImpl(
     {
       children,
       tabBarPosition = 'top',
@@ -34,7 +34,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
       testID,
     }: React.PropsWithChildren<Props>,
     ref,
-  ) => {
+  ) {
     const [selectedPage, setSelectedPage] = React.useState(0)
     const pagerView = React.useRef<PagerView>(null)
 
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index 7be2b11ec..fe4febbb7 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -14,51 +14,49 @@ interface Props {
   renderTabBar: RenderTabBarFn
   onPageSelected?: (index: number) => void
 }
-export const Pager = React.forwardRef(
-  (
-    {
-      children,
-      tabBarPosition = 'top',
-      initialPage = 0,
-      renderTabBar,
-      onPageSelected,
-    }: React.PropsWithChildren<Props>,
-    ref,
-  ) => {
-    const [selectedPage, setSelectedPage] = React.useState(initialPage)
+export const Pager = React.forwardRef(function PagerImpl(
+  {
+    children,
+    tabBarPosition = 'top',
+    initialPage = 0,
+    renderTabBar,
+    onPageSelected,
+  }: React.PropsWithChildren<Props>,
+  ref,
+) {
+  const [selectedPage, setSelectedPage] = React.useState(initialPage)
 
-    React.useImperativeHandle(ref, () => ({
-      setPage: (index: number) => setSelectedPage(index),
-    }))
+  React.useImperativeHandle(ref, () => ({
+    setPage: (index: number) => setSelectedPage(index),
+  }))
 
-    const onTabBarSelect = React.useCallback(
-      (index: number) => {
-        setSelectedPage(index)
-        onPageSelected?.(index)
-      },
-      [setSelectedPage, onPageSelected],
-    )
+  const onTabBarSelect = React.useCallback(
+    (index: number) => {
+      setSelectedPage(index)
+      onPageSelected?.(index)
+    },
+    [setSelectedPage, onPageSelected],
+  )
 
-    return (
-      <View>
-        {tabBarPosition === 'top' &&
-          renderTabBar({
-            selectedPage,
-            onSelect: onTabBarSelect,
-          })}
-        {React.Children.map(children, (child, i) => (
-          <View
-            style={selectedPage === i ? undefined : s.hidden}
-            key={`page-${i}`}>
-            {child}
-          </View>
-        ))}
-        {tabBarPosition === 'bottom' &&
-          renderTabBar({
-            selectedPage,
-            onSelect: onTabBarSelect,
-          })}
-      </View>
-    )
-  },
-)
+  return (
+    <View>
+      {tabBarPosition === 'top' &&
+        renderTabBar({
+          selectedPage,
+          onSelect: onTabBarSelect,
+        })}
+      {React.Children.map(children, (child, i) => (
+        <View
+          style={selectedPage === i ? undefined : s.hidden}
+          key={`page-${i}`}>
+          {child}
+        </View>
+      ))}
+      {tabBarPosition === 'bottom' &&
+        renderTabBar({
+          selectedPage,
+          onSelect: onTabBarSelect,
+        })}
+    </View>
+  )
+})
diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx
index 440d912af..6f9fff52f 100644
--- a/src/view/com/search/Suggestions.tsx
+++ b/src/view/com/search/Suggestions.tsx
@@ -39,179 +39,177 @@ interface ProfileView {
 type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
 
 export const Suggestions = observer(
-  forwardRef(
-    (
-      {
-        foafs,
-        suggestedActors,
-      }: {
-        foafs: FoafsModel
-        suggestedActors: SuggestedActorsModel
-      },
-      flatListRef: ForwardedRef<FlatList>,
-    ) => {
-      const pal = usePalette('default')
-      const [refreshing, setRefreshing] = React.useState(false)
-      const data = React.useMemo(() => {
-        let items: Item[] = []
+  forwardRef(function SuggestionsImpl(
+    {
+      foafs,
+      suggestedActors,
+    }: {
+      foafs: FoafsModel
+      suggestedActors: SuggestedActorsModel
+    },
+    flatListRef: ForwardedRef<FlatList>,
+  ) {
+    const pal = usePalette('default')
+    const [refreshing, setRefreshing] = React.useState(false)
+    const data = React.useMemo(() => {
+      let items: Item[] = []
 
-        if (foafs.popular.length > 0) {
-          items = items
-            .concat([
-              {
-                _reactKey: '__popular_heading__',
-                type: 'heading',
-                title: 'In Your Network',
-              },
-            ])
-            .concat(
-              foafs.popular.map(ref => ({
-                _reactKey: `popular-${ref.did}`,
-                type: 'ref',
-                ref,
-              })),
-            )
-        }
-        if (suggestedActors.hasContent) {
-          items = items
-            .concat([
-              {
-                _reactKey: '__suggested_heading__',
-                type: 'heading',
-                title: 'Suggested Follows',
-              },
-            ])
-            .concat(
-              suggestedActors.suggestions.map(suggested => ({
-                _reactKey: `suggested-${suggested.did}`,
-                type: 'suggested',
-                suggested,
-              })),
-            )
-        }
-        for (const source of foafs.sources) {
-          const item = foafs.foafs.get(source)
-          if (!item || item.follows.length === 0) {
-            continue
-          }
-          items = items
-            .concat([
-              {
-                _reactKey: `__${item.did}_heading__`,
-                type: 'heading',
-                title: `Followed by ${sanitizeDisplayName(
-                  item.displayName || sanitizeHandle(item.handle),
-                )}`,
-              },
-            ])
-            .concat(
-              item.follows.slice(0, 10).map(view => ({
-                _reactKey: `${item.did}-${view.did}`,
-                type: 'profile-view',
-                view,
-              })),
-            )
+      if (foafs.popular.length > 0) {
+        items = items
+          .concat([
+            {
+              _reactKey: '__popular_heading__',
+              type: 'heading',
+              title: 'In Your Network',
+            },
+          ])
+          .concat(
+            foafs.popular.map(ref => ({
+              _reactKey: `popular-${ref.did}`,
+              type: 'ref',
+              ref,
+            })),
+          )
+      }
+      if (suggestedActors.hasContent) {
+        items = items
+          .concat([
+            {
+              _reactKey: '__suggested_heading__',
+              type: 'heading',
+              title: 'Suggested Follows',
+            },
+          ])
+          .concat(
+            suggestedActors.suggestions.map(suggested => ({
+              _reactKey: `suggested-${suggested.did}`,
+              type: 'suggested',
+              suggested,
+            })),
+          )
+      }
+      for (const source of foafs.sources) {
+        const item = foafs.foafs.get(source)
+        if (!item || item.follows.length === 0) {
+          continue
         }
+        items = items
+          .concat([
+            {
+              _reactKey: `__${item.did}_heading__`,
+              type: 'heading',
+              title: `Followed by ${sanitizeDisplayName(
+                item.displayName || sanitizeHandle(item.handle),
+              )}`,
+            },
+          ])
+          .concat(
+            item.follows.slice(0, 10).map(view => ({
+              _reactKey: `${item.did}-${view.did}`,
+              type: 'profile-view',
+              view,
+            })),
+          )
+      }
 
-        return items
-      }, [
-        foafs.popular,
-        suggestedActors.hasContent,
-        suggestedActors.suggestions,
-        foafs.sources,
-        foafs.foafs,
-      ])
+      return items
+    }, [
+      foafs.popular,
+      suggestedActors.hasContent,
+      suggestedActors.suggestions,
+      foafs.sources,
+      foafs.foafs,
+    ])
 
-      const onRefresh = React.useCallback(async () => {
-        setRefreshing(true)
-        try {
-          await foafs.fetch()
-        } finally {
-          setRefreshing(false)
-        }
-      }, [foafs, setRefreshing])
+    const onRefresh = React.useCallback(async () => {
+      setRefreshing(true)
+      try {
+        await foafs.fetch()
+      } finally {
+        setRefreshing(false)
+      }
+    }, [foafs, setRefreshing])
 
-      const renderItem = React.useCallback(
-        ({item}: {item: Item}) => {
-          if (item.type === 'heading') {
-            return (
-              <Text type="title" style={[styles.heading, pal.text]}>
-                {item.title}
-              </Text>
-            )
-          }
-          if (item.type === 'ref') {
-            return (
-              <View style={[styles.card, pal.view, pal.border]}>
-                <ProfileCardWithFollowBtn
-                  key={item.ref.did}
-                  profile={item.ref}
-                  noBg
-                  noBorder
-                  followers={
-                    item.ref.followers
-                      ? (item.ref.followers as AppBskyActorDefs.ProfileView[])
-                      : undefined
-                  }
-                />
-              </View>
-            )
-          }
-          if (item.type === 'profile-view') {
-            return (
-              <View style={[styles.card, pal.view, pal.border]}>
-                <ProfileCardWithFollowBtn
-                  key={item.view.did}
-                  profile={item.view}
-                  noBg
-                  noBorder
-                />
-              </View>
-            )
-          }
-          if (item.type === 'suggested') {
-            return (
-              <View style={[styles.card, pal.view, pal.border]}>
-                <ProfileCardWithFollowBtn
-                  key={item.suggested.did}
-                  profile={item.suggested}
-                  noBg
-                  noBorder
-                />
-              </View>
-            )
-          }
-          return null
-        },
-        [pal],
-      )
+    const renderItem = React.useCallback(
+      ({item}: {item: Item}) => {
+        if (item.type === 'heading') {
+          return (
+            <Text type="title" style={[styles.heading, pal.text]}>
+              {item.title}
+            </Text>
+          )
+        }
+        if (item.type === 'ref') {
+          return (
+            <View style={[styles.card, pal.view, pal.border]}>
+              <ProfileCardWithFollowBtn
+                key={item.ref.did}
+                profile={item.ref}
+                noBg
+                noBorder
+                followers={
+                  item.ref.followers
+                    ? (item.ref.followers as AppBskyActorDefs.ProfileView[])
+                    : undefined
+                }
+              />
+            </View>
+          )
+        }
+        if (item.type === 'profile-view') {
+          return (
+            <View style={[styles.card, pal.view, pal.border]}>
+              <ProfileCardWithFollowBtn
+                key={item.view.did}
+                profile={item.view}
+                noBg
+                noBorder
+              />
+            </View>
+          )
+        }
+        if (item.type === 'suggested') {
+          return (
+            <View style={[styles.card, pal.view, pal.border]}>
+              <ProfileCardWithFollowBtn
+                key={item.suggested.did}
+                profile={item.suggested}
+                noBg
+                noBorder
+              />
+            </View>
+          )
+        }
+        return null
+      },
+      [pal],
+    )
 
-      if (foafs.isLoading || suggestedActors.isLoading) {
-        return (
-          <CenteredView>
-            <ProfileCardFeedLoadingPlaceholder />
-          </CenteredView>
-        )
-      }
+    if (foafs.isLoading || suggestedActors.isLoading) {
       return (
-        <FlatList
-          ref={flatListRef}
-          data={data}
-          keyExtractor={item => item._reactKey}
-          refreshControl={
-            <RefreshControl
-              refreshing={refreshing}
-              onRefresh={onRefresh}
-              tintColor={pal.colors.text}
-              titleColor={pal.colors.text}
-            />
-          }
-          renderItem={renderItem}
-          initialNumToRender={15}
-        />
+        <CenteredView>
+          <ProfileCardFeedLoadingPlaceholder />
+        </CenteredView>
       )
-    },
-  ),
+    }
+    return (
+      <FlatList
+        ref={flatListRef}
+        data={data}
+        keyExtractor={item => item._reactKey}
+        refreshControl={
+          <RefreshControl
+            refreshing={refreshing}
+            onRefresh={onRefresh}
+            tintColor={pal.colors.text}
+            titleColor={pal.colors.text}
+          />
+        }
+        renderItem={renderItem}
+        initialNumToRender={15}
+      />
+    )
+  }),
 )
 
 const styles = StyleSheet.create({
diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx
index 09ccb6a2d..77276f184 100644
--- a/src/view/com/util/PressableWithHover.tsx
+++ b/src/view/com/util/PressableWithHover.tsx
@@ -12,34 +12,32 @@ interface PressableWithHover extends PressableProps {
   hoverStyle: StyleProp<ViewStyle>
 }
 
-export const PressableWithHover = forwardRef(
-  (
-    {
-      children,
-      style,
-      hoverStyle,
-      ...props
-    }: PropsWithChildren<PressableWithHover>,
-    ref: Ref<any>,
-  ) => {
-    const [isHovering, setIsHovering] = useState(false)
+export const PressableWithHover = forwardRef(function PressableWithHoverImpl(
+  {
+    children,
+    style,
+    hoverStyle,
+    ...props
+  }: PropsWithChildren<PressableWithHover>,
+  ref: Ref<any>,
+) {
+  const [isHovering, setIsHovering] = useState(false)
 
-    const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
-    const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
-    style =
-      typeof style !== 'function' && isHovering
-        ? addStyle(style, hoverStyle)
-        : style
+  const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
+  const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
+  style =
+    typeof style !== 'function' && isHovering
+      ? addStyle(style, hoverStyle)
+      : style
 
-    return (
-      <Pressable
-        {...props}
-        style={style}
-        onHoverIn={onHoverIn}
-        onHoverOut={onHoverOut}
-        ref={ref}>
-        {children}
-      </Pressable>
-    )
-  },
-)
+  return (
+    <Pressable
+      {...props}
+      style={style}
+      onHoverIn={onHoverIn}
+      onHoverOut={onHoverOut}
+      ref={ref}>
+      {children}
+    </Pressable>
+  )
+})
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx
index 8d2a30506..6c0e4c6cc 100644
--- a/src/view/com/util/ViewSelector.tsx
+++ b/src/view/com/util/ViewSelector.tsx
@@ -42,100 +42,98 @@ export const ViewSelector = React.forwardRef<
     onRefresh?: () => void
     onEndReached?: (info: {distanceFromEnd: number}) => void
   }
->(
-  (
-    {
-      sections,
-      items,
-      refreshing,
-      renderHeader,
-      renderItem,
-      ListFooterComponent,
-      onSelectView,
-      onScroll,
-      onRefresh,
-      onEndReached,
-    },
-    ref,
-  ) => {
-    const pal = usePalette('default')
-    const [selectedIndex, setSelectedIndex] = useState<number>(0)
-    const flatListRef = React.useRef<FlatList>(null)
+>(function ViewSelectorImpl(
+  {
+    sections,
+    items,
+    refreshing,
+    renderHeader,
+    renderItem,
+    ListFooterComponent,
+    onSelectView,
+    onScroll,
+    onRefresh,
+    onEndReached,
+  },
+  ref,
+) {
+  const pal = usePalette('default')
+  const [selectedIndex, setSelectedIndex] = useState<number>(0)
+  const flatListRef = React.useRef<FlatList>(null)
 
-    // events
-    // =
+  // events
+  // =
 
-    const keyExtractor = React.useCallback((item: any) => item._reactKey, [])
+  const keyExtractor = React.useCallback((item: any) => item._reactKey, [])
 
-    const onPressSelection = React.useCallback(
-      (index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
-      [setSelectedIndex, sections],
-    )
-    useEffect(() => {
-      onSelectView?.(selectedIndex)
-    }, [selectedIndex, onSelectView])
+  const onPressSelection = React.useCallback(
+    (index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
+    [setSelectedIndex, sections],
+  )
+  useEffect(() => {
+    onSelectView?.(selectedIndex)
+  }, [selectedIndex, onSelectView])
 
-    React.useImperativeHandle(ref, () => ({
-      scrollToTop: () => {
-        flatListRef.current?.scrollToOffset({offset: 0})
-      },
-    }))
+  React.useImperativeHandle(ref, () => ({
+    scrollToTop: () => {
+      flatListRef.current?.scrollToOffset({offset: 0})
+    },
+  }))
 
-    // rendering
-    // =
+  // rendering
+  // =
 
-    const renderItemInternal = React.useCallback(
-      ({item}: {item: any}) => {
-        if (item === HEADER_ITEM) {
-          if (renderHeader) {
-            return renderHeader()
-          }
-          return <View />
-        } else if (item === SELECTOR_ITEM) {
-          return (
-            <Selector
-              items={sections}
-              selectedIndex={selectedIndex}
-              onSelect={onPressSelection}
-            />
-          )
-        } else {
-          return renderItem(item)
+  const renderItemInternal = React.useCallback(
+    ({item}: {item: any}) => {
+      if (item === HEADER_ITEM) {
+        if (renderHeader) {
+          return renderHeader()
         }
-      },
-      [sections, selectedIndex, onPressSelection, renderHeader, renderItem],
-    )
-
-    const data = React.useMemo(
-      () => [HEADER_ITEM, SELECTOR_ITEM, ...items],
-      [items],
-    )
-    return (
-      <FlatList
-        ref={flatListRef}
-        data={data}
-        keyExtractor={keyExtractor}
-        renderItem={renderItemInternal}
-        ListFooterComponent={ListFooterComponent}
-        // NOTE sticky header disabled on android due to major performance issues -prf
-        stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
-        onScroll={onScroll}
-        onEndReached={onEndReached}
-        refreshControl={
-          <RefreshControl
-            refreshing={refreshing!}
-            onRefresh={onRefresh}
-            tintColor={pal.colors.text}
+        return <View />
+      } else if (item === SELECTOR_ITEM) {
+        return (
+          <Selector
+            items={sections}
+            selectedIndex={selectedIndex}
+            onSelect={onPressSelection}
           />
-        }
-        onEndReachedThreshold={0.6}
-        contentContainerStyle={s.contentContainer}
-        removeClippedSubviews={true}
-        scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
-      />
-    )
-  },
-)
+        )
+      } else {
+        return renderItem(item)
+      }
+    },
+    [sections, selectedIndex, onPressSelection, renderHeader, renderItem],
+  )
+
+  const data = React.useMemo(
+    () => [HEADER_ITEM, SELECTOR_ITEM, ...items],
+    [items],
+  )
+  return (
+    <FlatList
+      ref={flatListRef}
+      data={data}
+      keyExtractor={keyExtractor}
+      renderItem={renderItemInternal}
+      ListFooterComponent={ListFooterComponent}
+      // NOTE sticky header disabled on android due to major performance issues -prf
+      stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
+      onScroll={onScroll}
+      onEndReached={onEndReached}
+      refreshControl={
+        <RefreshControl
+          refreshing={refreshing!}
+          onRefresh={onRefresh}
+          tintColor={pal.colors.text}
+        />
+      }
+      onEndReachedThreshold={0.6}
+      contentContainerStyle={s.contentContainer}
+      removeClippedSubviews={true}
+      scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
+    />
+  )
+})
 
 export function Selector({
   selectedIndex,
diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx
index 891d3f2ee..fda0a9b86 100644
--- a/src/view/com/util/Views.web.tsx
+++ b/src/view/com/util/Views.web.tsx
@@ -38,7 +38,7 @@ export function CenteredView({
   return <View style={style} {...props} />
 }
 
-export const FlatList = React.forwardRef(function <ItemT>(
+export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
   {
     contentContainerStyle,
     style,
@@ -99,7 +99,7 @@ export const FlatList = React.forwardRef(function <ItemT>(
   )
 })
 
-export const ScrollView = React.forwardRef(function (
+export const ScrollView = React.forwardRef(function ScrollViewImpl(
   {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
   ref: React.Ref<RNScrollView>,
 ) {
diff --git a/src/view/com/util/anim/TriggerableAnimated.tsx b/src/view/com/util/anim/TriggerableAnimated.tsx
index 2a3cbb957..eedeeda03 100644
--- a/src/view/com/util/anim/TriggerableAnimated.tsx
+++ b/src/view/com/util/anim/TriggerableAnimated.tsx
@@ -26,7 +26,7 @@ type PropsInner = TriggerableAnimatedProps & {
 export const TriggerableAnimated = React.forwardRef<
   TriggerableAnimatedRef,
   TriggerableAnimatedProps
->(({children, ...props}, ref) => {
+>(function TriggerableAnimatedImpl({children, ...props}, ref) {
   const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
     undefined,
   )
diff --git a/src/view/com/util/layouts/withBreakpoints.tsx b/src/view/com/util/layouts/withBreakpoints.tsx
index dc3f50dc9..5746aa660 100644
--- a/src/view/com/util/layouts/withBreakpoints.tsx
+++ b/src/view/com/util/layouts/withBreakpoints.tsx
@@ -2,13 +2,12 @@ import React from 'react'
 import {isNative} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 
-export const withBreakpoints =
-  <P extends object>(
-    Mobile: React.ComponentType<P>,
-    Tablet: React.ComponentType<P>,
-    Desktop: React.ComponentType<P>,
-  ): React.FC<P> =>
-  (props: P) => {
+export const withBreakpoints = <P extends object>(
+  Mobile: React.ComponentType<P>,
+  Tablet: React.ComponentType<P>,
+  Desktop: React.ComponentType<P>,
+): React.FC<P> =>
+  function WithBreakpoints(props: P) {
     const {isMobile, isTabletOrMobile} = useWebMediaQueries()
 
     if (isMobile || isNative) {