about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/constants.ts14
-rw-r--r--src/view/com/auth/onboarding/Welcome.tsx9
-rw-r--r--src/view/com/composer/photos/OpenCameraBtn.tsx6
-rw-r--r--src/view/com/composer/photos/SelectPhotoBtn.tsx5
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx3
-rw-r--r--src/view/com/pager/FeedsTabBarMobile.tsx5
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx12
-rw-r--r--src/view/com/profile/ProfileHeader.tsx58
-rw-r--r--src/view/com/search/HeaderWithInput.tsx5
-rw-r--r--src/view/com/util/UserAvatar.tsx128
-rw-r--r--src/view/com/util/UserBanner.tsx134
-rw-r--r--src/view/com/util/forms/DropdownButton.tsx125
-rw-r--r--src/view/com/util/forms/NativeDropdown.tsx250
-rw-r--r--src/view/com/util/forms/PostDropdownBtn.tsx148
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtn.web.tsx7
-rw-r--r--src/view/com/util/load-latest/LoadLatestBtnMobile.tsx5
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx54
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx3
-rw-r--r--src/view/screens/CustomFeed.tsx27
-rw-r--r--src/view/screens/Settings.tsx20
20 files changed, 693 insertions, 325 deletions
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 34da35e4f..001cdf8c3 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,3 +1,5 @@
+import {Insets} from 'react-native'
+
 const HELP_DESK_LANG = 'en-us'
 export const HELP_DESK_URL = `https://blueskyweb.zendesk.com/hc/${HELP_DESK_LANG}`
 
@@ -134,3 +136,15 @@ export function LINK_META_PROXY(serviceUrl: string) {
 }
 
 export const STATUS_PAGE_URL = 'https://status.bsky.app/'
+
+// Hitslop constants
+export const createHitslop = (size: number): Insets => ({
+  top: size,
+  left: size,
+  bottom: size,
+  right: size,
+})
+export const HITSLOP_10 = createHitslop(10)
+export const HITSLOP_20 = createHitslop(20)
+export const HITSLOP_30 = createHitslop(30)
+export const BACK_HITSLOP = HITSLOP_30
diff --git a/src/view/com/auth/onboarding/Welcome.tsx b/src/view/com/auth/onboarding/Welcome.tsx
index e7c068ea0..87435c88a 100644
--- a/src/view/com/auth/onboarding/Welcome.tsx
+++ b/src/view/com/auth/onboarding/Welcome.tsx
@@ -10,7 +10,7 @@ export const Welcome = ({next}: {next: () => void}) => {
   const pal = usePalette('default')
   return (
     <View style={[styles.container]}>
-      <View>
+      <View testID="welcomeScreen">
         <Text style={[pal.text, styles.title]}>Welcome to </Text>
         <Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
 
@@ -52,7 +52,12 @@ export const Welcome = ({next}: {next: () => void}) => {
         </View>
       </View>
 
-      <Button onPress={next} label="Continue" labelStyle={styles.buttonText} />
+      <Button
+        onPress={next}
+        label="Continue"
+        testID="continueBtn"
+        labelStyle={styles.buttonText}
+      />
     </View>
   )
 }
diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx
index 0f955984d..d58b17c58 100644
--- a/src/view/com/composer/photos/OpenCameraBtn.tsx
+++ b/src/view/com/composer/photos/OpenCameraBtn.tsx
@@ -10,11 +10,9 @@ import {useStores} from 'state/index'
 import {isDesktopWeb} from 'platform/detection'
 import {openCamera} from 'lib/media/picker'
 import {useCameraPermission} from 'lib/hooks/usePermissions'
-import {POST_IMG_MAX} from 'lib/constants'
+import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants'
 import {GalleryModel} from 'state/models/media/gallery'
 
-const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
-
 type Props = {
   gallery: GalleryModel
 }
@@ -54,7 +52,7 @@ export function OpenCameraBtn({gallery}: Props) {
       testID="openCameraButton"
       onPress={onPressTakePicture}
       style={styles.button}
-      hitSlop={HITSLOP}
+      hitSlop={HITSLOP_10}
       accessibilityRole="button"
       accessibilityLabel="Camera"
       accessibilityHint="Opens camera on device">
diff --git a/src/view/com/composer/photos/SelectPhotoBtn.tsx b/src/view/com/composer/photos/SelectPhotoBtn.tsx
index aaf0477c7..081456f75 100644
--- a/src/view/com/composer/photos/SelectPhotoBtn.tsx
+++ b/src/view/com/composer/photos/SelectPhotoBtn.tsx
@@ -9,8 +9,7 @@ import {useAnalytics} from 'lib/analytics/analytics'
 import {isDesktopWeb} from 'platform/detection'
 import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
 import {GalleryModel} from 'state/models/media/gallery'
-
-const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
+import {HITSLOP_10} from 'lib/constants'
 
 type Props = {
   gallery: GalleryModel
@@ -36,7 +35,7 @@ export function SelectPhotoBtn({gallery}: Props) {
       testID="openGalleryBtn"
       onPress={onPressSelectPhotos}
       style={styles.button}
-      hitSlop={HITSLOP}
+      hitSlop={HITSLOP_10}
       accessibilityRole="button"
       accessibilityLabel="Gallery"
       accessibilityHint="Opens device photo gallery">
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
index 84e5f90fb..c95538c55 100644
--- a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -6,6 +6,7 @@
  *
  */
 
+import {createHitslop} from 'lib/constants'
 import React from 'react'
 import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
 
@@ -13,7 +14,7 @@ type Props = {
   onRequestClose: () => void
 }
 
-const HIT_SLOP = {top: 16, left: 16, bottom: 16, right: 16}
+const HIT_SLOP = createHitslop(16)
 
 const ImageDefaultHeader = ({onRequestClose}: Props) => (
   <SafeAreaView style={styles.root}>
diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx
index 621173567..55a38803f 100644
--- a/src/view/com/pager/FeedsTabBarMobile.tsx
+++ b/src/view/com/pager/FeedsTabBarMobile.tsx
@@ -12,6 +12,7 @@ import {Text} from '../util/text/Text'
 import {CogIcon} from 'lib/icons'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {s} from 'lib/styles'
+import {HITSLOP_10} from 'lib/constants'
 
 export const FeedsTabBar = observer(
   (
@@ -54,7 +55,7 @@ export const FeedsTabBar = observer(
               accessibilityRole="button"
               accessibilityLabel="Open navigation"
               accessibilityHint="Access profile and other navigation links"
-              hitSlop={10}>
+              hitSlop={HITSLOP_10}>
               <FontAwesomeIcon
                 icon="bars"
                 size={18}
@@ -68,7 +69,7 @@ export const FeedsTabBar = observer(
           <View style={[pal.view]}>
             <Link
               href="/settings/saved-feeds"
-              hitSlop={10}
+              hitSlop={HITSLOP_10}
               accessibilityRole="button"
               accessibilityLabel="Edit Saved Feeds"
               accessibilityHint="Opens screen to edit Saved Feeds">
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 0680bbc06..edf8d7749 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -11,7 +11,7 @@ import {PostThreadItemModel} from 'state/models/content/post-thread-item'
 import {Link} from '../util/Link'
 import {RichText} from '../util/text/RichText'
 import {Text} from '../util/text/Text'
-import {PostDropdownBtn} from '../util/forms/DropdownButton'
+import {PostDropdownBtn} from '../util/forms/PostDropdownBtn'
 import * as Toast from '../util/Toast'
 import {PreviewableUserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
@@ -202,7 +202,6 @@ export const PostThreadItem = observer(function PostThreadItem({
               <View style={s.flex1} />
               <PostDropdownBtn
                 testID="postDropdownBtn"
-                style={[styles.metaItem, s.mt2, s.px5]}
                 itemUri={itemUri}
                 itemCid={itemCid}
                 itemHref={itemHref}
@@ -212,13 +211,8 @@ export const PostThreadItem = observer(function PostThreadItem({
                 onCopyPostText={onCopyPostText}
                 onOpenTranslate={onOpenTranslate}
                 onToggleThreadMute={onToggleThreadMute}
-                onDeletePost={onDeletePost}>
-                <FontAwesomeIcon
-                  icon="ellipsis-h"
-                  size={14}
-                  style={[pal.textLight]}
-                />
-              </PostDropdownBtn>
+                onDeletePost={onDeletePost}
+              />
             </View>
             <View style={styles.meta}>
               <Link
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index b54aa64f4..a372f0d81 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -17,7 +17,6 @@ import {toShareUrl} from 'lib/strings/url-helpers'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {s, colors} from 'lib/styles'
-import {DropdownButton, DropdownItem} from '../util/forms/DropdownButton'
 import * as Toast from '../util/Toast'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {Text} from '../util/text/Text'
@@ -36,11 +35,11 @@ import {FollowState} from 'state/models/cache/my-follows'
 import {shareUrl} from 'lib/sharing'
 import {formatCount} from '../util/numeric/format'
 import {navigate} from '../../../Navigation'
+import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
+import {BACK_HITSLOP} from 'lib/constants'
 import {isInvalidHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
 
-const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
-
 interface Props {
   view: ProfileModel
   onRefreshAll: () => void
@@ -260,15 +259,29 @@ const ProfileHeaderLoaded = observer(
           testID: 'profileHeaderDropdownShareBtn',
           label: 'Share',
           onPress: onPressShare,
+          icon: {
+            ios: {
+              name: 'square.and.arrow.up',
+            },
+            android: 'ic_menu_share',
+            web: 'share',
+          },
         },
       ]
       if (!isMe) {
-        items.push({sep: true})
+        items.push({label: 'separator'})
         // Only add "Add to Lists" on other user's profiles, doesn't make sense to mute my own self!
         items.push({
           testID: 'profileHeaderDropdownListAddRemoveBtn',
           label: 'Add to Lists',
           onPress: onPressAddRemoveLists,
+          icon: {
+            ios: {
+              name: 'list.bullet',
+            },
+            android: 'ic_menu_add',
+            web: 'list',
+          },
         })
         if (!view.viewer.blocking) {
           items.push({
@@ -277,6 +290,13 @@ const ProfileHeaderLoaded = observer(
             onPress: view.viewer.muted
               ? onPressUnmuteAccount
               : onPressMuteAccount,
+            icon: {
+              ios: {
+                name: 'speaker.slash',
+              },
+              android: 'ic_lock_silent_mode',
+              web: 'comment-slash',
+            },
           })
         }
         items.push({
@@ -285,11 +305,25 @@ const ProfileHeaderLoaded = observer(
           onPress: view.viewer.blocking
             ? onPressUnblockAccount
             : onPressBlockAccount,
+          icon: {
+            ios: {
+              name: 'person.fill.xmark',
+            },
+            android: 'ic_menu_close_clear_cancel',
+            web: 'user-slash',
+          },
         })
         items.push({
           testID: 'profileHeaderDropdownReportBtn',
           label: 'Report Account',
           onPress: onPressReportAccount,
+          icon: {
+            ios: {
+              name: 'exclamationmark.triangle',
+            },
+            android: 'ic_menu_report_image',
+            web: 'circle-exclamation',
+          },
         })
       }
       return items
@@ -380,13 +414,17 @@ const ProfileHeaderLoaded = observer(
               </>
             ) : null}
             {dropdownItems?.length ? (
-              <DropdownButton
+              <NativeDropdown
                 testID="profileHeaderDropdownBtn"
-                type="bare"
-                items={dropdownItems}
-                style={[styles.btn, styles.secondaryBtn, pal.btn]}>
-                <FontAwesomeIcon icon="ellipsis" style={[pal.text]} />
-              </DropdownButton>
+                items={dropdownItems}>
+                <View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
+                  <FontAwesomeIcon
+                    icon="ellipsis"
+                    size={20}
+                    style={[pal.text]}
+                  />
+                </View>
+              </NativeDropdown>
             ) : undefined}
           </View>
           <View>
diff --git a/src/view/com/search/HeaderWithInput.tsx b/src/view/com/search/HeaderWithInput.tsx
index 2ec079dde..f825c578e 100644
--- a/src/view/com/search/HeaderWithInput.tsx
+++ b/src/view/com/search/HeaderWithInput.tsx
@@ -10,8 +10,7 @@ import {useTheme} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useStores} from 'state/index'
 import {useAnalytics} from 'lib/analytics/analytics'
-
-const MENU_HITSLOP = {left: 10, top: 10, right: 30, bottom: 10}
+import {HITSLOP_10} from 'lib/constants'
 
 interface Props {
   isInputFocused: boolean
@@ -55,7 +54,7 @@ export function HeaderWithInput({
         <TouchableOpacity
           testID="viewHeaderBackOrMenuBtn"
           onPress={onPressMenu}
-          hitSlop={MENU_HITSLOP}
+          hitSlop={HITSLOP_10}
           style={styles.headerMenuBtn}
           accessibilityRole="button"
           accessibilityLabel="Menu"
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index eb6405f10..d999ffb31 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -2,7 +2,6 @@ import React, {useMemo} from 'react'
 import {StyleSheet, View} from 'react-native'
 import Svg, {Circle, Rect, Path} from 'react-native-svg'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {HighPriorityImage} from 'view/com/util/images/Image'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
 import {
@@ -11,12 +10,12 @@ import {
 } from 'lib/hooks/usePermissions'
 import {useStores} from 'state/index'
 import {colors} from 'lib/styles'
-import {DropdownButton} from './forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {AvatarModeration} from 'lib/labeling/types'
 import {UserPreviewLink} from './UserPreviewLink'
+import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
 
 type Type = 'user' | 'algo' | 'list'
 
@@ -130,59 +129,81 @@ export function UserAvatar({
   }, [type, size])
 
   const dropdownItems = useMemo(
-    () => [
-      !isWeb && {
-        testID: 'changeAvatarCameraBtn',
-        label: 'Camera',
-        icon: 'camera' as IconProp,
-        onPress: async () => {
-          if (!(await requestCameraAccessIfNeeded())) {
-            return
-          }
+    () =>
+      [
+        !isWeb && {
+          testID: 'changeAvatarCameraBtn',
+          label: 'Camera',
+          icon: {
+            ios: {
+              name: 'camera',
+            },
+            android: 'ic_menu_camera',
+            web: 'camera',
+          },
+          onPress: async () => {
+            if (!(await requestCameraAccessIfNeeded())) {
+              return
+            }
 
-          onSelectNewAvatar?.(
-            await openCamera(store, {
-              width: 1000,
-              height: 1000,
-              cropperCircleOverlay: true,
-            }),
-          )
+            onSelectNewAvatar?.(
+              await openCamera(store, {
+                width: 1000,
+                height: 1000,
+                cropperCircleOverlay: true,
+              }),
+            )
+          },
         },
-      },
-      {
-        testID: 'changeAvatarLibraryBtn',
-        label: 'Library',
-        icon: 'image' as IconProp,
-        onPress: async () => {
-          if (!(await requestPhotoAccessIfNeeded())) {
-            return
-          }
+        {
+          testID: 'changeAvatarLibraryBtn',
+          label: 'Library',
+          icon: {
+            ios: {
+              name: 'photo.on.rectangle.angled',
+            },
+            android: 'ic_menu_gallery',
+            web: 'gallery',
+          },
+          onPress: async () => {
+            if (!(await requestPhotoAccessIfNeeded())) {
+              return
+            }
 
-          const items = await openPicker({
-            aspect: [1, 1],
-          })
-          const item = items[0]
+            const items = await openPicker({
+              aspect: [1, 1],
+            })
+            const item = items[0]
 
-          const croppedImage = await openCropper(store, {
-            mediaType: 'photo',
-            cropperCircleOverlay: true,
-            height: item.height,
-            width: item.width,
-            path: item.path,
-          })
+            const croppedImage = await openCropper(store, {
+              mediaType: 'photo',
+              cropperCircleOverlay: true,
+              height: item.height,
+              width: item.width,
+              path: item.path,
+            })
 
-          onSelectNewAvatar?.(croppedImage)
+            onSelectNewAvatar?.(croppedImage)
+          },
         },
-      },
-      !!avatar && {
-        testID: 'changeAvatarRemoveBtn',
-        label: 'Remove',
-        icon: ['far', 'trash-can'] as IconProp,
-        onPress: async () => {
-          onSelectNewAvatar?.(null)
+        !!avatar && {
+          label: 'separator',
         },
-      },
-    ],
+        !!avatar && {
+          testID: 'changeAvatarRemoveBtn',
+          label: 'Remove',
+          icon: {
+            ios: {
+              name: 'trash',
+            },
+            android: 'ic_delete',
+            web: 'trash',
+          },
+          onPress: async () => {
+            onSelectNewAvatar?.(null)
+          },
+        },
+      ].filter(Boolean) as DropdownItem[],
     [
       avatar,
       onSelectNewAvatar,
@@ -209,14 +230,7 @@ export function UserAvatar({
 
   // onSelectNewAvatar is only passed as prop on the EditProfile component
   return onSelectNewAvatar ? (
-    <DropdownButton
-      testID="changeAvatarBtn"
-      type="bare"
-      items={dropdownItems}
-      openToRight
-      rightOffset={-10}
-      bottomOffset={-10}
-      menuWidth={170}>
+    <NativeDropdown testID="changeAvatarBtn" items={dropdownItems}>
       {avatar ? (
         <HighPriorityImage
           testID="userAvatarImage"
@@ -234,7 +248,7 @@ export function UserAvatar({
           color={pal.text.color as string}
         />
       </View>
-    </DropdownButton>
+    </NativeDropdown>
   ) : avatar &&
     !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
     <View style={{width: size, height: size}}>
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index cce0e839b..b7e91b5dd 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -1,7 +1,6 @@
-import React from 'react'
+import React, {useMemo} from 'react'
 import {StyleSheet, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {IconProp} from '@fortawesome/fontawesome-svg-core'
 import {Image} from 'expo-image'
 import {colors} from 'lib/styles'
 import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
@@ -10,11 +9,11 @@ import {
   usePhotoLibraryPermission,
   useCameraPermission,
 } from 'lib/hooks/usePermissions'
-import {DropdownButton} from './forms/DropdownButton'
 import {usePalette} from 'lib/hooks/usePalette'
 import {AvatarModeration} from 'lib/labeling/types'
 import {isWeb, isAndroid} from 'platform/detection'
 import {Image as RNImage} from 'react-native-image-crop-picker'
+import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
 
 export function UserBanner({
   banner,
@@ -30,63 +29,84 @@ export function UserBanner({
   const {requestCameraAccessIfNeeded} = useCameraPermission()
   const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
 
-  const dropdownItems = [
-    !isWeb && {
-      testID: 'changeBannerCameraBtn',
-      label: 'Camera',
-      icon: 'camera' as IconProp,
-      onPress: async () => {
-        if (!(await requestCameraAccessIfNeeded())) {
-          return
-        }
-        onSelectNewBanner?.(
-          await openCamera(store, {
-            width: 3000,
-            height: 1000,
-          }),
-        )
-      },
-    },
-    {
-      testID: 'changeBannerLibraryBtn',
-      label: 'Library',
-      icon: 'image' as IconProp,
-      onPress: async () => {
-        if (!(await requestPhotoAccessIfNeeded())) {
-          return
-        }
-        const items = await openPicker()
+  const dropdownItems: DropdownItem[] = useMemo(
+    () =>
+      [
+        !isWeb && {
+          testID: 'changeBannerCameraBtn',
+          label: 'Camera',
+          icon: {
+            ios: {
+              name: 'camera',
+            },
+            android: 'ic_menu_camera',
+            web: 'camera',
+          },
+          onPress: async () => {
+            if (!(await requestCameraAccessIfNeeded())) {
+              return
+            }
+            onSelectNewBanner?.(
+              await openCamera(store, {
+                width: 3000,
+                height: 1000,
+              }),
+            )
+          },
+        },
+        {
+          testID: 'changeBannerLibraryBtn',
+          label: 'Library',
+          icon: {
+            ios: {
+              name: 'photo.on.rectangle.angled',
+            },
+            android: 'ic_menu_gallery',
+            web: 'gallery',
+          },
+          onPress: async () => {
+            if (!(await requestPhotoAccessIfNeeded())) {
+              return
+            }
+            const items = await openPicker()
 
-        onSelectNewBanner?.(
-          await openCropper(store, {
-            mediaType: 'photo',
-            path: items[0].path,
-            width: 3000,
-            height: 1000,
-          }),
-        )
-      },
-    },
-    !!banner && {
-      testID: 'changeBannerRemoveBtn',
-      label: 'Remove',
-      icon: ['far', 'trash-can'] as IconProp,
-      onPress: () => {
-        onSelectNewBanner?.(null)
-      },
-    },
-  ]
+            onSelectNewBanner?.(
+              await openCropper(store, {
+                mediaType: 'photo',
+                path: items[0].path,
+                width: 3000,
+                height: 1000,
+              }),
+            )
+          },
+        },
+        !!banner && {
+          testID: 'changeBannerRemoveBtn',
+          label: 'Remove',
+          icon: {
+            ios: {
+              name: 'trash',
+            },
+            android: 'ic_delete',
+            web: 'trash',
+          },
+          onPress: () => {
+            onSelectNewBanner?.(null)
+          },
+        },
+      ].filter(Boolean) as DropdownItem[],
+    [
+      banner,
+      onSelectNewBanner,
+      requestCameraAccessIfNeeded,
+      requestPhotoAccessIfNeeded,
+      store,
+    ],
+  )
 
   // setUserBanner is only passed as prop on the EditProfile component
   return onSelectNewBanner ? (
-    <DropdownButton
-      testID="changeBannerBtn"
-      type="bare"
-      items={dropdownItems}
-      openToRight
-      rightOffset={-200}
-      bottomOffset={-10}
-      menuWidth={170}>
+    <NativeDropdown testID="changeBannerBtn" items={dropdownItems}>
       {banner ? (
         <Image
           testID="userBannerImage"
@@ -109,7 +129,7 @@ export function UserBanner({
           color={pal.text.color as string}
         />
       </View>
-    </DropdownButton>
+    </NativeDropdown>
   ) : banner &&
     !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
     <Image
diff --git a/src/view/com/util/forms/DropdownButton.tsx b/src/view/com/util/forms/DropdownButton.tsx
index 046610b29..a1ee3d589 100644
--- a/src/view/com/util/forms/DropdownButton.tsx
+++ b/src/view/com/util/forms/DropdownButton.tsx
@@ -14,14 +14,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../text/Text'
 import {Button, ButtonType} from './Button'
 import {colors} from 'lib/styles'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useTheme} from 'lib/ThemeContext'
-import {isWeb} from 'platform/detection'
-import {shareUrl} from 'lib/sharing'
+import {HITSLOP_10} from 'lib/constants'
 
-const HITSLOP = {left: 10, top: 10, right: 10, bottom: 10}
 const ESTIMATED_BTN_HEIGHT = 50
 const ESTIMATED_SEP_HEIGHT = 16
 const ESTIMATED_HEADING_HEIGHT = 60
@@ -140,7 +136,7 @@ export function DropdownButton({
         testID={testID}
         style={style}
         onPress={onPress}
-        hitSlop={HITSLOP}
+        hitSlop={HITSLOP_10}
         ref={ref1}
         accessibilityRole="button"
         accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`}
@@ -163,112 +159,6 @@ export function DropdownButton({
   )
 }
 
-export function PostDropdownBtn({
-  testID,
-  style,
-  children,
-  itemUri,
-  itemCid,
-  itemHref,
-  isAuthor,
-  isThreadMuted,
-  onCopyPostText,
-  onOpenTranslate,
-  onToggleThreadMute,
-  onDeletePost,
-}: {
-  testID?: string
-  style?: StyleProp<ViewStyle>
-  children?: React.ReactNode
-  itemUri: string
-  itemCid: string
-  itemHref: string
-  itemTitle: string
-  isAuthor: boolean
-  isThreadMuted: boolean
-  onCopyPostText: () => void
-  onOpenTranslate: () => void
-  onToggleThreadMute: () => void
-  onDeletePost: () => void
-}) {
-  const store = useStores()
-
-  const dropdownItems: DropdownItem[] = [
-    {
-      testID: 'postDropdownTranslateBtn',
-      icon: 'language',
-      label: 'Translate...',
-      onPress() {
-        onOpenTranslate()
-      },
-    },
-    {
-      testID: 'postDropdownCopyTextBtn',
-      icon: ['far', 'paste'],
-      label: 'Copy post text',
-      onPress() {
-        onCopyPostText()
-      },
-    },
-    {
-      testID: 'postDropdownShareBtn',
-      icon: 'share',
-      label: 'Share...',
-      onPress() {
-        const url = toShareUrl(itemHref)
-        shareUrl(url)
-      },
-    },
-    {sep: true},
-    {
-      testID: 'postDropdownMuteThreadBtn',
-      icon: 'comment-slash',
-      label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
-      onPress() {
-        onToggleThreadMute()
-      },
-    },
-    {sep: true},
-    !isAuthor && {
-      testID: 'postDropdownReportBtn',
-      icon: 'circle-exclamation',
-      label: 'Report post',
-      onPress() {
-        store.shell.openModal({
-          name: 'report-post',
-          postUri: itemUri,
-          postCid: itemCid,
-        })
-      },
-    },
-    isAuthor && {
-      testID: 'postDropdownDeleteBtn',
-      icon: ['far', 'trash-can'],
-      label: 'Delete post',
-      onPress() {
-        store.shell.openModal({
-          name: 'confirm',
-          title: 'Delete this post?',
-          message: 'Are you sure? This can not be undone.',
-          onPressConfirm: onDeletePost,
-        })
-      },
-    },
-  ].filter(Boolean) as DropdownItem[]
-
-  return (
-    <DropdownButton
-      testID={testID}
-      style={style}
-      items={dropdownItems}
-      menuWidth={isWeb ? 220 : 200}
-      accessibilityLabel="Additional post actions"
-      accessibilityHint="">
-      {children}
-    </DropdownButton>
-  )
-}
-
 function createDropdownMenu(
   x: number,
   y: number,
@@ -324,15 +214,16 @@ const DropdownItems = ({
 
   const numItems = items.filter(isBtn).length
 
+  // TODO: Refactor dropdown components to:
+  // - (On web, if not handled by React Native) use semantic <select />
+  // and <option /> elements for keyboard navigation out of the box
+  // - (On mobile) be buttons by default, accept `label` and `nativeID`
+  // props, and always have an explicit label
   return (
     <>
+      {/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
       <TouchableWithoutFeedback
         onPress={onOuterPress}
-        // TODO: Refactor dropdown components to:
-        // - (On web, if not handled by React Native) use semantic <select />
-        // and <option /> elements for keyboard navigation out of the box
-        // - (On mobile) be buttons by default, accept `label` and `nativeID`
-        // props, and always have an explicit label
         accessibilityRole="button"
         accessibilityLabel="Toggle dropdown"
         accessibilityHint="">
diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx
new file mode 100644
index 000000000..d8f16ce19
--- /dev/null
+++ b/src/view/com/util/forms/NativeDropdown.tsx
@@ -0,0 +1,250 @@
+import React from 'react'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import * as DropdownMenu from 'zeego/dropdown-menu'
+import {
+  Pressable,
+  StyleSheet,
+  Platform,
+  StyleProp,
+  ViewStyle,
+} from 'react-native'
+import {IconProp} from '@fortawesome/fontawesome-svg-core'
+import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
+import {usePalette} from 'lib/hooks/usePalette'
+import {isWeb} from 'platform/detection'
+import {useTheme} from 'lib/ThemeContext'
+import {HITSLOP_10} from 'lib/constants'
+
+// Custom Dropdown Menu Components
+// ==
+export const DropdownMenuRoot = DropdownMenu.Root
+export const DropdownMenuTrigger = DropdownMenu.Trigger
+export const DropdownMenuContent = DropdownMenu.Content
+type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
+export const DropdownMenuItem = DropdownMenu.create(
+  (props: ItemProps & {testID?: string}) => {
+    const pal = usePalette('default')
+    const theme = useTheme()
+    const [focused, setFocused] = React.useState(false)
+    const {borderColor: backgroundColor} =
+      theme.colorScheme === 'dark' ? pal.borderDark : pal.border
+
+    return (
+      <DropdownMenu.Item
+        {...props}
+        style={[styles.item, focused && {backgroundColor: backgroundColor}]}
+        onFocus={() => {
+          setFocused(true)
+          props.onFocus && props.onFocus()
+        }}
+        onBlur={() => {
+          setFocused(false)
+          props.onBlur && props.onBlur()
+        }}
+      />
+    )
+  },
+  'Item',
+)
+type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
+export const DropdownMenuItemTitle = DropdownMenu.create(
+  (props: TitleProps) => {
+    const pal = usePalette('default')
+    return (
+      <DropdownMenu.ItemTitle
+        {...props}
+        style={[props.style, pal.text, styles.itemTitle]}
+      />
+    )
+  },
+  'ItemTitle',
+)
+type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
+export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
+  return <DropdownMenu.ItemIcon {...props} />
+}, 'ItemIcon')
+type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
+export const DropdownMenuSeparator = DropdownMenu.create(
+  (props: SeparatorProps) => {
+    const pal = usePalette('default')
+    const theme = useTheme()
+    const {borderColor: separatorColor} =
+      theme.colorScheme === 'dark' ? pal.borderDark : pal.border
+    return (
+      <DropdownMenu.Separator
+        {...props}
+        style={[
+          props.style,
+          styles.separator,
+          {backgroundColor: separatorColor},
+        ]}
+      />
+    )
+  },
+  'Separator',
+)
+
+// Types for Dropdown Menu and Items
+export type DropdownItem = {
+  label: string | 'separator'
+  onPress?: () => void
+  testID?: string
+  icon?: {
+    ios: MenuItemCommonProps['ios']
+    android: string
+    web: IconProp
+  }
+}
+type Props = {
+  items: DropdownItem[]
+  children?: React.ReactNode
+  testID?: string
+}
+
+/* The `NativeDropdown` function uses native iOS and Android dropdown menus.
+ * It also creates a animated custom dropdown for web that uses
+ * Radix UI primitives under the hood
+ * @prop {DropdownItem[]} items - An array of dropdown items
+ * @prop {React.ReactNode} children - A custom dropdown trigger
+ */
+export function NativeDropdown({items, children, testID}: Props) {
+  const pal = usePalette('default')
+  const theme = useTheme()
+  const dropDownBackgroundColor =
+    theme.colorScheme === 'dark' ? pal.btn : pal.viewLight
+  const defaultCtrlColor = React.useMemo(
+    () => ({
+      color: theme.palette.default.postCtrl,
+    }),
+    [theme],
+  ) as StyleProp<ViewStyle>
+
+  return (
+    <DropdownMenuRoot>
+      <DropdownMenuTrigger action="press">
+        <Pressable
+          testID={testID}
+          accessibilityRole="button"
+          style={({pressed}) => [{opacity: pressed ? 0.5 : 1}]}
+          hitSlop={HITSLOP_10}>
+          {children ? (
+            children
+          ) : (
+            <FontAwesomeIcon
+              icon="ellipsis"
+              size={20}
+              style={[defaultCtrlColor, styles.ellipsis]}
+            />
+          )}
+        </Pressable>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent
+        style={[styles.content, dropDownBackgroundColor]}
+        loop>
+        {items.map((item, index) => {
+          if (item.label === 'separator') {
+            return (
+              <DropdownMenuSeparator
+                key={getKey(item.label, index, item.testID)}
+              />
+            )
+          }
+          if (index > 1 && items[index - 1].label === 'separator') {
+            return (
+              <DropdownMenu.Group key={getKey(item.label, index, item.testID)}>
+                <DropdownMenuItem
+                  key={getKey(item.label, index, item.testID)}
+                  onSelect={item.onPress}>
+                  <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle>
+                  {item.icon && (
+                    <DropdownMenuItemIcon
+                      ios={item.icon.ios}
+                      // androidIconName={item.icon.android} TODO: Add custom android icon support, because these ones are based on https://developer.android.com/reference/android/R.drawable.html and they are ugly
+                    >
+                      <FontAwesomeIcon
+                        icon={item.icon.web}
+                        size={20}
+                        style={[pal.text]}
+                      />
+                    </DropdownMenuItemIcon>
+                  )}
+                </DropdownMenuItem>
+              </DropdownMenu.Group>
+            )
+          }
+          return (
+            <DropdownMenuItem
+              key={getKey(item.label, index, item.testID)}
+              onSelect={item.onPress}>
+              <DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle>
+              {item.icon && (
+                <DropdownMenuItemIcon
+                  ios={item.icon.ios}
+                  // androidIconName={item.icon.android}
+                >
+                  <FontAwesomeIcon
+                    icon={item.icon.web}
+                    size={20}
+                    style={[pal.text]}
+                  />
+                </DropdownMenuItemIcon>
+              )}
+            </DropdownMenuItem>
+          )
+        })}
+      </DropdownMenuContent>
+    </DropdownMenuRoot>
+  )
+}
+
+const getKey = (label: string, index: number, id?: string) => {
+  if (id) {
+    return id
+  }
+  return `${label}_${index}`
+}
+
+const styles = StyleSheet.create({
+  separator: {
+    height: 1,
+    marginVertical: 4,
+  },
+  ellipsis: {
+    padding: isWeb ? 0 : 10,
+  },
+  content: {
+    backgroundColor: '#f0f0f0',
+    borderRadius: 8,
+    paddingVertical: 4,
+    paddingHorizontal: 4,
+    marginTop: 6,
+    ...Platform.select({
+      web: {
+        animationDuration: '400ms',
+        animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
+        willChange: 'transform, opacity',
+        animationKeyframes: {
+          '0%': {opacity: 0, transform: [{scale: 0.5}]},
+          '100%': {opacity: 1, transform: [{scale: 1}]},
+        },
+        boxShadow:
+          '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
+        transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)',
+      },
+    }),
+  },
+  item: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    columnGap: 20,
+    // @ts-ignore -web
+    cursor: 'pointer',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderRadius: 8,
+  },
+  itemTitle: {
+    fontSize: 18,
+  },
+})
diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx
new file mode 100644
index 000000000..ad9ba1619
--- /dev/null
+++ b/src/view/com/util/forms/PostDropdownBtn.tsx
@@ -0,0 +1,148 @@
+import React from 'react'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {useStores} from 'state/index'
+import {shareUrl} from 'lib/sharing'
+import {
+  NativeDropdown,
+  DropdownItem as NativeDropdownItem,
+} from './NativeDropdown'
+import {Pressable} from 'react-native'
+
+export function PostDropdownBtn({
+  testID,
+  itemUri,
+  itemCid,
+  itemHref,
+  isAuthor,
+  isThreadMuted,
+  onCopyPostText,
+  onOpenTranslate,
+  onToggleThreadMute,
+  onDeletePost,
+}: {
+  testID: string
+  itemUri: string
+  itemCid: string
+  itemHref: string
+  itemTitle: string
+  isAuthor: boolean
+  isThreadMuted: boolean
+  onCopyPostText: () => void
+  onOpenTranslate: () => void
+  onToggleThreadMute: () => void
+  onDeletePost: () => void
+}) {
+  const store = useStores()
+
+  const dropdownItems: NativeDropdownItem[] = [
+    {
+      label: 'Translate',
+      onPress() {
+        onOpenTranslate()
+      },
+      testID: 'postDropdownTranslateBtn',
+      icon: {
+        ios: {
+          name: 'character.book.closed',
+        },
+        android: 'ic_menu_sort_alphabetically',
+        web: 'language',
+      },
+    },
+    {
+      label: 'Copy post text',
+      onPress() {
+        onCopyPostText()
+      },
+      testID: 'postDropdownCopyTextBtn',
+      icon: {
+        ios: {
+          name: 'doc.on.doc',
+        },
+        android: 'ic_menu_edit',
+        web: ['far', 'paste'],
+      },
+    },
+    {
+      label: 'Share',
+      onPress() {
+        const url = toShareUrl(itemHref)
+        shareUrl(url)
+      },
+      testID: 'postDropdownShareBtn',
+      icon: {
+        ios: {
+          name: 'square.and.arrow.up',
+        },
+        android: 'ic_menu_share',
+        web: 'share',
+      },
+    },
+    {
+      label: 'separator',
+    },
+    {
+      label: isThreadMuted ? 'Unmute thread' : 'Mute thread',
+      onPress() {
+        onToggleThreadMute()
+      },
+      testID: 'postDropdownMuteThreadBtn',
+      icon: {
+        ios: {
+          name: 'speaker.slash',
+        },
+        android: 'ic_lock_silent_mode',
+        web: 'comment-slash',
+      },
+    },
+    {
+      label: 'separator',
+    },
+    {
+      label: 'Report post',
+      onPress() {
+        store.shell.openModal({
+          name: 'report-post',
+          postUri: itemUri,
+          postCid: itemCid,
+        })
+      },
+      testID: 'postDropdownReportBtn',
+      icon: {
+        ios: {
+          name: 'exclamationmark.triangle',
+        },
+        android: 'ic_menu_report_image',
+        web: 'circle-exclamation',
+      },
+    },
+    isAuthor && {
+      label: 'separator',
+    },
+    isAuthor && {
+      label: 'Delete post',
+      onPress() {
+        store.shell.openModal({
+          name: 'confirm',
+          title: 'Delete this post?',
+          message: 'Are you sure? This can not be undone.',
+          onPressConfirm: onDeletePost,
+        })
+      },
+      testID: 'postDropdownDeleteBtn',
+      icon: {
+        ios: {
+          name: 'trash',
+        },
+        android: 'ic_menu_delete',
+        web: ['far', 'trash-can'],
+      },
+    },
+  ].filter(Boolean) as NativeDropdownItem[]
+
+  return (
+    <Pressable testID={testID} accessibilityRole="button">
+      <NativeDropdown items={dropdownItems} />
+    </Pressable>
+  )
+}
diff --git a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
index fefc540c0..c90e5dfb1 100644
--- a/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtn.web.tsx
@@ -5,8 +5,7 @@ import {Text} from '../text/Text'
 import {usePalette} from 'lib/hooks/usePalette'
 import {LoadLatestBtn as LoadLatestBtnMobile} from './LoadLatestBtnMobile'
 import {isMobileWeb} from 'platform/detection'
-
-const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
+import {HITSLOP_20} from 'lib/constants'
 
 export const LoadLatestBtn = ({
   onPress,
@@ -40,7 +39,7 @@ export const LoadLatestBtn = ({
             minimalShellMode && styles.loadLatestCenteredMinimal,
           ]}
           onPress={onPress}
-          hitSlop={HITSLOP}
+          hitSlop={HITSLOP_20}
           accessibilityRole="button"
           accessibilityLabel={label}
           accessibilityHint="">
@@ -52,7 +51,7 @@ export const LoadLatestBtn = ({
       <TouchableOpacity
         style={[pal.view, pal.borderDark, styles.loadLatest]}
         onPress={onPress}
-        hitSlop={HITSLOP}
+        hitSlop={HITSLOP_20}
         accessibilityRole="button"
         accessibilityLabel={label}
         accessibilityHint="">
diff --git a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
index 412b9b803..eb7eaaa49 100644
--- a/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
+++ b/src/view/com/util/load-latest/LoadLatestBtnMobile.tsx
@@ -7,8 +7,7 @@ import {clamp} from 'lodash'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {colors} from 'lib/styles'
-
-const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
+import {HITSLOP_20} from 'lib/constants'
 
 export const LoadLatestBtn = observer(
   ({
@@ -35,7 +34,7 @@ export const LoadLatestBtn = observer(
           },
         ]}
         onPress={onPress}
-        hitSlop={HITSLOP}
+        hitSlop={HITSLOP_20}
         accessibilityRole="button"
         accessibilityLabel={label}
         accessibilityHint="">
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index c544f6409..672e02693 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -6,17 +6,13 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {
-  FontAwesomeIcon,
-  FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
 // DISABLED see #135
 // import {
 //   TriggerableAnimated,
 //   TriggerableAnimatedRef,
 // } from './anim/TriggerableAnimated'
 import {Text} from '../text/Text'
-import {PostDropdownBtn} from '../forms/DropdownButton'
+import {PostDropdownBtn} from '../forms/PostDropdownBtn'
 import {HeartIcon, HeartIconSolid, CommentBottomArrow} from 'lib/icons'
 import {s, colors} from 'lib/styles'
 import {pluralize} from 'lib/strings/helpers'
@@ -24,6 +20,7 @@ import {useTheme} from 'lib/ThemeContext'
 import {useStores} from 'state/index'
 import {RepostButton} from './RepostButton'
 import {Haptics} from 'lib/haptics'
+import {createHitslop} from 'lib/constants'
 
 interface PostCtrlsOpts {
   itemUri: string
@@ -56,7 +53,7 @@ interface PostCtrlsOpts {
   onDeletePost: () => void
 }
 
-const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
+const HITSLOP = createHitslop(5)
 
 // DISABLED see #135
 /*
@@ -222,36 +219,21 @@ export function PostCtrls(opts: PostCtrlsOpts) {
           </Text>
         ) : undefined}
       </TouchableOpacity>
-      <View>
-        {opts.big ? undefined : (
-          <PostDropdownBtn
-            testID="postDropdownBtn"
-            style={styles.ctrl}
-            itemUri={opts.itemUri}
-            itemCid={opts.itemCid}
-            itemHref={opts.itemHref}
-            itemTitle={opts.itemTitle}
-            isAuthor={opts.isAuthor}
-            isThreadMuted={opts.isThreadMuted}
-            onCopyPostText={opts.onCopyPostText}
-            onOpenTranslate={opts.onOpenTranslate}
-            onToggleThreadMute={opts.onToggleThreadMute}
-            onDeletePost={opts.onDeletePost}>
-            <FontAwesomeIcon
-              icon="ellipsis-h"
-              size={18}
-              style={[
-                s.mt2,
-                s.mr5,
-                {
-                  color:
-                    theme.colorScheme === 'light' ? colors.gray4 : colors.gray5,
-                } as FontAwesomeIconStyle,
-              ]}
-            />
-          </PostDropdownBtn>
-        )}
-      </View>
+      {opts.big ? undefined : (
+        <PostDropdownBtn
+          testID="postDropdownBtn"
+          itemUri={opts.itemUri}
+          itemCid={opts.itemCid}
+          itemHref={opts.itemHref}
+          itemTitle={opts.itemTitle}
+          isAuthor={opts.isAuthor}
+          isThreadMuted={opts.isThreadMuted}
+          onCopyPostText={opts.onCopyPostText}
+          onOpenTranslate={opts.onOpenTranslate}
+          onToggleThreadMute={opts.onToggleThreadMute}
+          onDeletePost={opts.onDeletePost}
+        />
+      )}
       {/* used for adding pad to the right side */}
       <View />
     </View>
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 4338e4c59..5fe62aefe 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -6,8 +6,9 @@ import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
 import {pluralize} from 'lib/strings/helpers'
 import {useStores} from 'state/index'
+import {createHitslop} from 'lib/constants'
 
-const HITSLOP = {top: 5, left: 5, bottom: 5, right: 5}
+const HITSLOP = createHitslop(5)
 
 interface Props {
   isReposted: boolean
diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx
index 61550c683..d5ecff042 100644
--- a/src/view/screens/CustomFeed.tsx
+++ b/src/view/screens/CustomFeed.tsx
@@ -29,10 +29,10 @@ import {Haptics} from 'lib/haptics'
 import {ComposeIcon2} from 'lib/icons'
 import {FAB} from '../com/util/fab/FAB'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {DropdownButton, DropdownItem} from 'view/com/util/forms/DropdownButton'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {EmptyState} from 'view/com/util/EmptyState'
 import {useAnalytics} from 'lib/analytics/analytics'
+import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
 import {makeProfileLink} from 'lib/routes/links'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'CustomFeed'>
@@ -121,11 +121,25 @@ export const CustomFeedScreen = withAuthRequired(
           testID: 'feedHeaderDropdownRemoveBtn',
           label: 'Remove from my feeds',
           onPress: onToggleSaved,
+          icon: {
+            ios: {
+              name: 'trash',
+            },
+            android: 'ic_delete',
+            web: 'trash',
+          },
         },
         {
           testID: 'feedHeaderDropdownShareBtn',
           label: 'Share link',
           onPress: onPressShare,
+          icon: {
+            ios: {
+              name: 'square.and.arrow.up',
+            },
+            android: 'ic_menu_share',
+            web: 'share',
+          },
         },
       ]
       return items
@@ -163,17 +177,10 @@ export const CustomFeedScreen = withAuthRequired(
             </Button>
           ) : undefined}
           {currentFeed?.isSaved ? (
-            <DropdownButton
+            <NativeDropdown
               testID="feedHeaderDropdownBtn"
-              type="default-light"
               items={dropdownItems}
-              menuWidth={250}>
-              <FontAwesomeIcon
-                icon="ellipsis"
-                color={pal.colors.textLight}
-                size={18}
-              />
-            </DropdownButton>
+            />
           ) : (
             <Button
               type="default-light"
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index dd456c35e..47aa65585 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -3,6 +3,7 @@ import {
   ActivityIndicator,
   Linking,
   Platform,
+  Pressable,
   StyleSheet,
   TextStyle,
   TouchableOpacity,
@@ -30,7 +31,6 @@ import {Link} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
 import * as Toast from '../com/util/Toast'
 import {UserAvatar} from '../com/util/UserAvatar'
-import {DropdownButton} from 'view/com/util/forms/DropdownButton'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
 import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -50,6 +50,7 @@ import {makeProfileLink} from 'lib/routes/links'
 // -prf
 import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
 import {STATUS_PAGE_URL} from 'lib/constants'
+import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
 export const SettingsScreen = withAuthRequired(
@@ -565,24 +566,31 @@ export const SettingsScreen = withAuthRequired(
 function AccountDropdownBtn({handle}: {handle: string}) {
   const store = useStores()
   const pal = usePalette('default')
-  const items = [
+  const items: DropdownItem[] = [
     {
       label: 'Remove account',
       onPress: () => {
         store.session.removeAccount(handle)
         Toast.show('Account removed from quick access')
       },
+      icon: {
+        ios: {
+          name: 'trash',
+        },
+        android: 'ic_delete',
+        web: 'trash',
+      },
     },
   ]
   return (
-    <View style={s.pl10}>
-      <DropdownButton type="bare" items={items}>
+    <Pressable accessibilityRole="button" style={s.pl10}>
+      <NativeDropdown testID="accountSettingsDropdownBtn" items={items}>
         <FontAwesomeIcon
           icon="ellipsis-h"
           style={pal.textLight as FontAwesomeIconStyle}
         />
-      </DropdownButton>
-    </View>
+      </NativeDropdown>
+    </Pressable>
   )
 }