about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-04-11 15:20:26 -0700
committerGitHub <noreply@github.com>2024-04-11 15:20:26 -0700
commit740cd029d7162a936d16b427201eb8972e365b94 (patch)
treeff4cf80cc8cd88bd958abd68c3cb3aa46328240e /src/view
parent9007810cdb5ffc8fbdf8e2a2af6c073b76b318f3 (diff)
downloadvoidsky-740cd029d7162a936d16b427201eb8972e365b94.tar.zst
Improve Android haptic, offer toggle for haptics in the app (#3482)
* improve android haptics, offer toggle for haptics

* update haptics.ts

* default to false

* simplify to `playHaptic`

* just leave them as `feedInfo`

* use a hook for `playHaptic`

* missed one of them
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx18
-rw-r--r--src/view/screens/ProfileFeed.tsx30
-rw-r--r--src/view/screens/ProfileList.tsx106
-rw-r--r--src/view/screens/SavedFeeds.tsx52
-rw-r--r--src/view/screens/Settings/index.tsx84
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx7
6 files changed, 140 insertions, 157 deletions
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 58874cd55..cd4a36373 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -16,7 +16,6 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
-import {Haptics} from '#/lib/haptics'
 import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons'
 import {makeProfileLink} from '#/lib/routes/links'
 import {shareUrl} from '#/lib/sharing'
@@ -32,6 +31,7 @@ import {
 } from '#/state/queries/post'
 import {useRequireAuth} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
+import {useHaptics} from 'lib/haptics'
 import {useDialogControl} from '#/components/Dialog'
 import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
 import * as Prompt from '#/components/Prompt'
@@ -67,6 +67,7 @@ let PostCtrls = ({
   )
   const requireAuth = useRequireAuth()
   const loggedOutWarningPromptControl = useDialogControl()
+  const playHaptic = useHaptics()
 
   const shouldShowLoggedOutWarning = React.useMemo(() => {
     return !!post.author.labels?.find(
@@ -84,7 +85,7 @@ let PostCtrls = ({
   const onPressToggleLike = React.useCallback(async () => {
     try {
       if (!post.viewer?.like) {
-        Haptics.default()
+        playHaptic()
         await queueLike()
       } else {
         await queueUnlike()
@@ -94,13 +95,13 @@ let PostCtrls = ({
         throw e
       }
     }
-  }, [post.viewer?.like, queueLike, queueUnlike])
+  }, [playHaptic, post.viewer?.like, queueLike, queueUnlike])
 
   const onRepost = useCallback(async () => {
     closeModal()
     try {
       if (!post.viewer?.repost) {
-        Haptics.default()
+        playHaptic()
         await queueRepost()
       } else {
         await queueUnrepost()
@@ -110,7 +111,7 @@ let PostCtrls = ({
         throw e
       }
     }
-  }, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal])
+  }, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost])
 
   const onQuote = useCallback(() => {
     closeModal()
@@ -123,15 +124,16 @@ let PostCtrls = ({
         indexedAt: post.indexedAt,
       },
     })
-    Haptics.default()
+    playHaptic()
   }, [
+    closeModal,
+    openComposer,
     post.uri,
     post.cid,
     post.author,
     post.indexedAt,
     record.text,
-    openComposer,
-    closeModal,
+    playHaptic,
   ])
 
   const onShare = useCallback(() => {
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 4560e14eb..814c1e855 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -27,7 +27,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
 import {useAnalytics} from 'lib/analytics/analytics'
-import {Haptics} from 'lib/haptics'
+import {useHaptics} from 'lib/haptics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useSetTitle} from 'lib/hooks/useSetTitle'
 import {ComposeIcon2} from 'lib/icons'
@@ -159,6 +159,7 @@ export function ProfileFeedScreenInner({
   const reportDialogControl = useReportDialogControl()
   const {openComposer} = useComposerControls()
   const {track} = useAnalytics()
+  const playHaptic = useHaptics()
   const feedSectionRef = React.useRef<SectionRef>(null)
   const isScreenFocused = useIsFocused()
 
@@ -201,7 +202,7 @@ export function ProfileFeedScreenInner({
 
   const onToggleSaved = React.useCallback(async () => {
     try {
-      Haptics.default()
+      playHaptic()
 
       if (isSaved) {
         await removeFeed({uri: feedInfo.uri})
@@ -221,18 +222,19 @@ export function ProfileFeedScreenInner({
       logger.error('Failed up update feeds', {message: err})
     }
   }, [
-    feedInfo,
+    playHaptic,
     isSaved,
-    saveFeed,
     removeFeed,
-    resetSaveFeed,
+    feedInfo,
     resetRemoveFeed,
     _,
+    saveFeed,
+    resetSaveFeed,
   ])
 
   const onTogglePinned = React.useCallback(async () => {
     try {
-      Haptics.default()
+      playHaptic()
 
       if (isPinned) {
         await unpinFeed({uri: feedInfo.uri})
@@ -245,7 +247,16 @@ export function ProfileFeedScreenInner({
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {message: e})
     }
-  }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _])
+  }, [
+    playHaptic,
+    isPinned,
+    unpinFeed,
+    feedInfo,
+    resetUnpinFeed,
+    pinFeed,
+    resetPinFeed,
+    _,
+  ])
 
   const onPressShare = React.useCallback(() => {
     const url = toShareUrl(feedInfo.route.href)
@@ -517,6 +528,7 @@ function AboutSection({
   const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
   const {hasSession} = useSession()
   const {track} = useAnalytics()
+  const playHaptic = useHaptics()
   const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
   const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
     useUnlikeMutation()
@@ -527,7 +539,7 @@ function AboutSection({
 
   const onToggleLiked = React.useCallback(async () => {
     try {
-      Haptics.default()
+      playHaptic()
 
       if (isLiked && likeUri) {
         await unlikeFeed({uri: likeUri})
@@ -546,7 +558,7 @@ function AboutSection({
       )
       logger.error('Failed up toggle like', {message: err})
     }
-  }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
+  }, [playHaptic, isLiked, likeUri, unlikeFeed, track, likeFeed, feedInfo, _])
 
   return (
     <View style={[styles.aboutSectionContainer]}>
diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx
index 58b89f239..1d93a9fd7 100644
--- a/src/view/screens/ProfileList.tsx
+++ b/src/view/screens/ProfileList.tsx
@@ -1,69 +1,70 @@
 import React, {useCallback, useMemo} from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
+import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useFocusEffect, useIsFocused} from '@react-navigation/native'
-import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
 import {useNavigation} from '@react-navigation/native'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api'
 import {useQueryClient} from '@tanstack/react-query'
-import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
-import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
-import {Feed} from 'view/com/posts/Feed'
-import {Text} from 'view/com/util/text/Text'
-import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
-import {CenteredView} from 'view/com/util/Views'
-import {EmptyState} from 'view/com/util/EmptyState'
-import {LoadingScreen} from 'view/com/util/LoadingScreen'
-import {RichText} from '#/components/RichText'
-import {Button} from 'view/com/util/forms/Button'
-import {TextLink} from 'view/com/util/Link'
-import {ListRef} from 'view/com/util/List'
-import * as Toast from 'view/com/util/Toast'
-import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {FAB} from 'view/com/util/fab/FAB'
-import {Haptics} from 'lib/haptics'
-import {FeedDescriptor} from '#/state/queries/post-feed'
-import {usePalette} from 'lib/hooks/usePalette'
-import {useSetTitle} from 'lib/hooks/useSetTitle'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
-import {NavigationProp} from 'lib/routes/types'
-import {toShareUrl} from 'lib/strings/url-helpers'
-import {shareUrl} from 'lib/sharing'
-import {s} from 'lib/styles'
-import {sanitizeHandle} from 'lib/strings/handles'
-import {makeProfileLink, makeListLink} from 'lib/routes/links'
-import {ComposeIcon2} from 'lib/icons'
-import {ListMembers} from '#/view/com/lists/ListMembers'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {useSetMinimalShellMode} from '#/state/shell'
+
+import {useAnalytics} from '#/lib/analytics/analytics'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {isNative, isWeb} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
 import {useModalControls} from '#/state/modals'
-import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
-import {useResolveUriQuery} from '#/state/queries/resolve-uri'
 import {
-  useListQuery,
-  useListMuteMutation,
   useListBlockMutation,
   useListDeleteMutation,
+  useListMuteMutation,
+  useListQuery,
 } from '#/state/queries/list'
-import {cleanError} from '#/lib/strings/errors'
-import {useSession} from '#/state/session'
-import {useComposerControls} from '#/state/shell/composer'
-import {isNative, isWeb} from '#/platform/detection'
-import {truncateAndInvalidate} from '#/state/queries/util'
+import {FeedDescriptor} from '#/state/queries/post-feed'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
 import {
-  usePreferencesQuery,
   usePinFeedMutation,
-  useUnpinFeedMutation,
+  usePreferencesQuery,
   useSetSaveFeedsMutation,
+  useUnpinFeedMutation,
 } from '#/state/queries/preferences'
-import {logger} from '#/logger'
-import {useAnalytics} from '#/lib/analytics/analytics'
-import {listenSoftReset} from '#/state/events'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {truncateAndInvalidate} from '#/state/queries/util'
+import {useSession} from '#/state/session'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useComposerControls} from '#/state/shell/composer'
+import {useHaptics} from 'lib/haptics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useSetTitle} from 'lib/hooks/useSetTitle'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {ComposeIcon2} from 'lib/icons'
+import {makeListLink, makeProfileLink} from 'lib/routes/links'
+import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
+import {NavigationProp} from 'lib/routes/types'
+import {shareUrl} from 'lib/sharing'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {toShareUrl} from 'lib/strings/url-helpers'
+import {s} from 'lib/styles'
+import {ListMembers} from '#/view/com/lists/ListMembers'
+import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
+import {Feed} from 'view/com/posts/Feed'
+import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
+import {EmptyState} from 'view/com/util/EmptyState'
+import {FAB} from 'view/com/util/fab/FAB'
+import {Button} from 'view/com/util/forms/Button'
+import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown'
+import {TextLink} from 'view/com/util/Link'
+import {ListRef} from 'view/com/util/List'
+import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
+import {LoadingScreen} from 'view/com/util/LoadingScreen'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {CenteredView} from 'view/com/util/Views'
 import {atoms as a, useTheme} from '#/alf'
-import * as Prompt from '#/components/Prompt'
 import {useDialogControl} from '#/components/Dialog'
+import * as Prompt from '#/components/Prompt'
+import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
+import {RichText} from '#/components/RichText'
 
 const SECTION_TITLES_CURATE = ['Posts', 'About']
 const SECTION_TITLES_MOD = ['About']
@@ -254,6 +255,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const {data: preferences} = usePreferencesQuery()
   const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
   const {track} = useAnalytics()
+  const playHaptic = useHaptics()
 
   const deleteListPromptControl = useDialogControl()
   const subscribeMutePromptControl = useDialogControl()
@@ -263,7 +265,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
   const isSaved = preferences?.feeds?.saved?.includes(list.uri)
 
   const onTogglePinned = React.useCallback(async () => {
-    Haptics.default()
+    playHaptic()
 
     try {
       if (isPinned) {
@@ -275,7 +277,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {message: e})
     }
-  }, [list.uri, isPinned, pinFeed, unpinFeed, _])
+  }, [playHaptic, isPinned, unpinFeed, list.uri, pinFeed, _])
 
   const onSubscribeMute = useCallback(async () => {
     try {
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 251c70638..0003dbd5d 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,31 +1,32 @@
 import React from 'react'
-import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native'
+import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
 import {track} from '#/lib/analytics/analytics'
-import {useAnalytics} from 'lib/analytics/analytics'
-import {usePalette} from 'lib/hooks/usePalette'
-import {CommonNavigatorParams} from 'lib/routes/types'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {ViewHeader} from 'view/com/util/ViewHeader'
-import {ScrollView, CenteredView} from 'view/com/util/Views'
-import {Text} from 'view/com/util/text/Text'
-import {s, colors} from 'lib/styles'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import * as Toast from 'view/com/util/Toast'
-import {Haptics} from 'lib/haptics'
-import {TextLink} from 'view/com/util/Link'
 import {logger} from '#/logger'
-import {useSetMinimalShellMode} from '#/state/shell'
-import {Trans, msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
 import {
-  usePreferencesQuery,
   usePinFeedMutation,
-  useUnpinFeedMutation,
+  usePreferencesQuery,
   useSetSaveFeedsMutation,
+  useUnpinFeedMutation,
 } from '#/state/queries/preferences'
+import {useSetMinimalShellMode} from '#/state/shell'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {useHaptics} from 'lib/haptics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {CommonNavigatorParams} from 'lib/routes/types'
+import {colors, s} from 'lib/styles'
+import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
+import {TextLink} from 'view/com/util/Link'
+import {Text} from 'view/com/util/text/Text'
+import * as Toast from 'view/com/util/Toast'
+import {ViewHeader} from 'view/com/util/ViewHeader'
+import {CenteredView, ScrollView} from 'view/com/util/Views'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -189,13 +190,14 @@ function ListItem({
 }) {
   const pal = usePalette('default')
   const {_} = useLingui()
+  const playHaptic = useHaptics()
   const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
   const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
     useUnpinFeedMutation()
   const isPending = isPinPending || isUnpinPending
 
   const onTogglePinned = React.useCallback(async () => {
-    Haptics.default()
+    playHaptic()
 
     try {
       resetSaveFeedsMutationState()
@@ -209,7 +211,15 @@ function ListItem({
       Toast.show(_(msg`There was an issue contacting the server`))
       logger.error('Failed to toggle pinned feed', {message: e})
     }
-  }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _])
+  }, [
+    playHaptic,
+    resetSaveFeedsMutationState,
+    isPinned,
+    unpinFeed,
+    feedUri,
+    pinFeed,
+    _,
+  ])
 
   const onPressUp = React.useCallback(async () => {
     if (!isPinned) return
diff --git a/src/view/screens/Settings/index.tsx b/src/view/screens/Settings/index.tsx
index 830a73ff2..8a7fa5e71 100644
--- a/src/view/screens/Settings/index.tsx
+++ b/src/view/screens/Settings/index.tsx
@@ -20,10 +20,9 @@ import {useLingui} from '@lingui/react'
 import {useFocusEffect, useNavigation} from '@react-navigation/native'
 import {useQueryClient} from '@tanstack/react-query'
 
-import {isNative} from '#/platform/detection'
+import {isIOS, isNative} from '#/platform/detection'
 import {useModalControls} from '#/state/modals'
 import {clearLegacyStorage} from '#/state/persisted/legacy'
-// TODO import {useInviteCodesQuery} from '#/state/queries/invites'
 import {clear as clearStorage} from '#/state/persisted/store'
 import {
   useRequireAltTextEnabled,
@@ -57,6 +56,10 @@ import {makeProfileLink} from 'lib/routes/links'
 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {NavigationProp} from 'lib/routes/types'
 import {colors, s} from 'lib/styles'
+import {
+  useHapticsDisabled,
+  useSetHapticsDisabled,
+} from 'state/preferences/disable-haptics'
 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
 import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
 import {ToggleButton} from 'view/com/util/forms/ToggleButton'
@@ -155,6 +158,8 @@ export function SettingsScreen({}: Props) {
   const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
   const inAppBrowserPref = useInAppBrowser()
   const setUseInAppBrowser = useSetInAppBrowser()
+  const isHapticsDisabled = useHapticsDisabled()
+  const setHapticsDisabled = useSetHapticsDisabled()
   const onboardingDispatch = useOnboardingDispatch()
   const navigation = useNavigation<NavigationProp>()
   const {isMobile} = useWebMediaQueries()
@@ -162,9 +167,6 @@ export function SettingsScreen({}: Props) {
   const {openModal} = useModalControls()
   const {isSwitchingAccounts, accounts, currentAccount} = useSession()
   const {mutate: clearPreferences} = useClearPreferencesMutation()
-  // TODO
-  // const {data: invites} = useInviteCodesQuery()
-  // const invitesAvailable = invites?.available?.length ?? 0
   const {setShowLoggedOut} = useLoggedOutViewControls()
   const closeAllActiveElements = useCloseAllActiveElements()
   const exportCarControl = useDialogControl()
@@ -220,13 +222,6 @@ export function SettingsScreen({}: Props) {
     exportCarControl.open()
   }, [exportCarControl])
 
-  /* TODO
-  const onPressInviteCodes = React.useCallback(() => {
-    track('Settings:InvitecodesButtonClicked')
-    openModal({name: 'invite-codes'})
-  }, [track, openModal])
- */
-
   const onPressLanguageSettings = React.useCallback(() => {
     navigation.navigate('LanguageSettings')
   }, [navigation])
@@ -414,58 +409,6 @@ export function SettingsScreen({}: Props) {
 
         <View style={styles.spacer20} />
 
-        {/* TODO (
-          <>
-            <Text type="xl-bold" style={[pal.text, styles.heading]}>
-              <Trans>Invite a Friend</Trans>
-            </Text>
-
-            <TouchableOpacity
-              testID="inviteFriendBtn"
-              style={[
-                styles.linkCard,
-                pal.view,
-                isSwitchingAccounts && styles.dimmed,
-              ]}
-              onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Invite`)}
-              accessibilityHint={_(msg`Opens invite code list`)}
-              disabled={invites?.disabled}>
-              <View
-                style={[
-                  styles.iconContainer,
-                  invitesAvailable > 0 ? primaryBg : pal.btn,
-                ]}>
-                <FontAwesomeIcon
-                  icon="ticket"
-                  style={
-                    (invitesAvailable > 0
-                      ? primaryText
-                      : pal.text) as FontAwesomeIconStyle
-                  }
-                />
-              </View>
-              <Text
-                type="lg"
-                style={invitesAvailable > 0 ? pal.link : pal.text}>
-                {invites?.disabled ? (
-                  <Trans>
-                    Your invite codes are hidden when logged in using an App
-                    Password
-                  </Trans>
-                ) : invitesAvailable === 1 ? (
-                  <Trans>{invitesAvailable} invite code available</Trans>
-                ) : (
-                  <Trans>{invitesAvailable} invite codes available</Trans>
-                )}
-              </Text>
-            </TouchableOpacity>
-
-            <View style={styles.spacer20} />
-          </>
-        )*/}
-
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
           <Trans>Accessibility</Trans>
         </Text>
@@ -738,6 +681,19 @@ export function SettingsScreen({}: Props) {
             />
           </View>
         )}
+        {isNative && (
+          <View style={[pal.view, styles.toggleCard]}>
+            <ToggleButton
+              type="default-light"
+              label={
+                isIOS ? _(msg`Disable haptics`) : _(msg`Disable vibrations`)
+              }
+              labelType="lg"
+              isSelected={isHapticsDisabled}
+              onPress={() => setHapticsDisabled(!isHapticsDisabled)}
+            />
+          </View>
+        )}
         <View style={styles.spacer20} />
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
           <Trans>Account</Trans>
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index f41631a96..c35fa106d 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -8,7 +8,7 @@ import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
 import {StackActions} from '@react-navigation/native'
 
 import {useAnalytics} from '#/lib/analytics/analytics'
-import {Haptics} from '#/lib/haptics'
+import {useHaptics} from '#/lib/haptics'
 import {useDedupe} from '#/lib/hooks/useDedupe'
 import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode'
 import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
@@ -59,6 +59,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
   const closeAllActiveElements = useCloseAllActiveElements()
   const dedupe = useDedupe()
   const accountSwitchControl = useDialogControl()
+  const playHaptic = useHaptics()
 
   const showSignIn = React.useCallback(() => {
     closeAllActiveElements()
@@ -104,9 +105,9 @@ export function BottomBar({navigation}: BottomTabBarProps) {
   }, [onPressTab])
 
   const onLongPressProfile = React.useCallback(() => {
-    Haptics.default()
+    playHaptic()
     accountSwitchControl.open()
-  }, [accountSwitchControl])
+  }, [accountSwitchControl, playHaptic])
 
   return (
     <>