From 740cd029d7162a936d16b427201eb8972e365b94 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 15:20:26 -0700 Subject: [PATCH] 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 --- patches/expo-haptics+12.8.1.md | 11 ++ patches/expo-haptics+12.8.1.patch | 13 ++ src/lib/haptics.ts | 45 ++----- .../Profile/Header/ProfileHeaderLabeler.tsx | 7 +- src/state/persisted/legacy.ts | 3 +- src/state/persisted/schema.ts | 3 + src/state/preferences/disable-haptics.tsx | 42 +++++++ src/state/preferences/index.tsx | 10 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 18 +-- src/view/screens/ProfileFeed.tsx | 30 +++-- src/view/screens/ProfileList.tsx | 114 +++++++++--------- src/view/screens/SavedFeeds.tsx | 52 ++++---- src/view/screens/Settings/index.tsx | 84 +++---------- src/view/shell/bottom-bar/BottomBar.tsx | 7 +- 14 files changed, 235 insertions(+), 204 deletions(-) create mode 100644 patches/expo-haptics+12.8.1.md create mode 100644 patches/expo-haptics+12.8.1.patch create mode 100644 src/state/preferences/disable-haptics.tsx diff --git a/patches/expo-haptics+12.8.1.md b/patches/expo-haptics+12.8.1.md new file mode 100644 index 00000000..afa7395b --- /dev/null +++ b/patches/expo-haptics+12.8.1.md @@ -0,0 +1,11 @@ +# Expo Haptics Patch + +Whenever we migrated to Expo Haptics, there was a difference between how the previous and new libraries handled the +Android implementation of an iOS "light" haptic. The previous library used the `Vibration` API solely, which does not +have any configuration for intensity of vibration. The `Vibration` API has also been deprecated since SDK 26. See: +https://github.com/mkuczera/react-native-haptic-feedback/blob/master/android/src/main/java/com/mkuczera/vibrateFactory/VibrateWithDuration.java + +Expo Haptics is using `VibrationManager` API on SDK >= 31. See: https://github.com/expo/expo/blob/main/packages/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt#L19 +The timing and intensity of their haptic configurations though differs greatly from the original implementation. This +patch uses the new `VibrationManager` API to create the same vibration that would have been seen in the deprecated +`Vibration` API. diff --git a/patches/expo-haptics+12.8.1.patch b/patches/expo-haptics+12.8.1.patch new file mode 100644 index 00000000..a95b56f3 --- /dev/null +++ b/patches/expo-haptics+12.8.1.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt +index 26c52af..b949a4c 100644 +--- a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt ++++ b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt +@@ -42,7 +42,7 @@ class HapticsModule : Module() { + + private fun vibrate(type: HapticsVibrationType) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { +- vibrator.vibrate(VibrationEffect.createWaveform(type.timings, type.amplitudes, -1)) ++ vibrator.vibrate(VibrationEffect.createWaveform(type.oldSDKPattern, intArrayOf(0, 100), -1)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(type.oldSDKPattern, -1) diff --git a/src/lib/haptics.ts b/src/lib/haptics.ts index b22d69d7..02940f79 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -1,47 +1,20 @@ -import { - impactAsync, - ImpactFeedbackStyle, - notificationAsync, - NotificationFeedbackType, - selectionAsync, -} from 'expo-haptics' +import React from 'react' +import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' import {isIOS, isWeb} from 'platform/detection' +import {useHapticsDisabled} from 'state/preferences/disable-haptics' const hapticImpact: ImpactFeedbackStyle = isIOS ? ImpactFeedbackStyle.Medium : ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s -export class Haptics { - static default() { - if (isWeb) { +export function useHaptics() { + const isHapticsDisabled = useHapticsDisabled() + + return React.useCallback(() => { + if (isHapticsDisabled || isWeb) { return } impactAsync(hapticImpact) - } - static impact(type: ImpactFeedbackStyle = hapticImpact) { - if (isWeb) { - return - } - impactAsync(type) - } - static selection() { - if (isWeb) { - return - } - selectionAsync() - } - static notification = (type: 'success' | 'warning' | 'error') => { - if (isWeb) { - return - } - switch (type) { - case 'success': - return notificationAsync(NotificationFeedbackType.Success) - case 'warning': - return notificationAsync(NotificationFeedbackType.Warning) - case 'error': - return notificationAsync(NotificationFeedbackType.Error) - } - } + }, [isHapticsDisabled]) } diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 4d8dbad8..d0fd5e20 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -10,7 +10,6 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Haptics} from '#/lib/haptics' import {isAppLabeler} from '#/lib/moderation' import {pluralize} from '#/lib/strings/helpers' import {logger} from '#/logger' @@ -21,6 +20,7 @@ import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' +import {useHaptics} from 'lib/haptics' import {useProfileShadow} from 'state/cache/profile-shadow' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' @@ -64,6 +64,7 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() + const playHaptic = useHaptics() const cantSubscribePrompt = Prompt.usePromptControl() const isSelf = currentAccount?.did === profile.did @@ -93,7 +94,7 @@ let ProfileHeaderLabeler = ({ return } try { - Haptics.default() + playHaptic() if (likeUri) { await unlikeMod({uri: likeUri}) @@ -114,7 +115,7 @@ let ProfileHeaderLabeler = ({ ) logger.error(`Failed to toggle labeler like`, {message: e.message}) } - }, [labeler, likeUri, likeMod, unlikeMod, track, _]) + }, [labeler, playHaptic, likeUri, unlikeMod, track, likeMod, _]) const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts index fd94a96a..ca7967cd 100644 --- a/src/state/persisted/legacy.ts +++ b/src/state/persisted/legacy.ts @@ -2,7 +2,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import {logger} from '#/logger' import {defaults, Schema, schema} from '#/state/persisted/schema' -import {write, read} from '#/state/persisted/store' +import {read, write} from '#/state/persisted/store' /** * The shape of the serialized data from our legacy Mobx store. @@ -113,6 +113,7 @@ export function transform(legacy: Partial): Schema { externalEmbeds: defaults.externalEmbeds, lastSelectedHomeFeed: defaults.lastSelectedHomeFeed, pdsAddressHistory: defaults.pdsAddressHistory, + disableHaptics: defaults.disableHaptics, } } diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 0aefaa47..67e082a9 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -1,4 +1,5 @@ import {z} from 'zod' + import {deviceLocales} from '#/platform/detection' const externalEmbedOptions = ['show', 'hide'] as const @@ -58,6 +59,7 @@ export const schema = z.object({ useInAppBrowser: z.boolean().optional(), lastSelectedHomeFeed: z.string().optional(), pdsAddressHistory: z.array(z.string()).optional(), + disableHaptics: z.boolean().optional(), }) export type Schema = z.infer @@ -93,4 +95,5 @@ export const defaults: Schema = { useInAppBrowser: undefined, lastSelectedHomeFeed: undefined, pdsAddressHistory: [], + disableHaptics: false, } diff --git a/src/state/preferences/disable-haptics.tsx b/src/state/preferences/disable-haptics.tsx new file mode 100644 index 00000000..af2c55a1 --- /dev/null +++ b/src/state/preferences/disable-haptics.tsx @@ -0,0 +1,42 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = boolean +type SetContext = (v: boolean) => void + +const stateContext = React.createContext( + Boolean(persisted.defaults.disableHaptics), +) +const setContext = React.createContext((_: boolean) => {}) + +export function Provider({children}: {children: React.ReactNode}) { + const [state, setState] = React.useState( + Boolean(persisted.get('disableHaptics')), + ) + + const setStateWrapped = React.useCallback( + (hapticsEnabled: persisted.Schema['disableHaptics']) => { + setState(Boolean(hapticsEnabled)) + persisted.write('disableHaptics', hapticsEnabled) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate(() => { + setState(Boolean(persisted.get('disableHaptics'))) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export const useHapticsDisabled = () => React.useContext(stateContext) +export const useSetHapticsDisabled = () => React.useContext(setContext) diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index cf1d9015..804d0fc3 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -1,11 +1,12 @@ import React from 'react' -import {Provider as LanguagesProvider} from './languages' + import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required' import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts' +import {Provider as DisableHapticsProvider} from './disable-haptics' import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' import {Provider as InAppBrowserProvider} from './in-app-browser' +import {Provider as LanguagesProvider} from './languages' -export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export { useRequireAltTextEnabled, useSetRequireAltTextEnabled, @@ -16,6 +17,7 @@ export { } from './external-embeds-prefs' export * from './hidden-posts' export {useLabelDefinitions} from './label-defs' +export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export function Provider({children}: React.PropsWithChildren<{}>) { return ( @@ -23,7 +25,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { - {children} + + {children} + diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 58874cd5..cd4a3637 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 4560e14e..814c1e85 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(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 ( diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 58b89f23..1d93a9fd 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 {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 {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 {useNavigation} from '@react-navigation/native' 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 {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 { + useListBlockMutation, + useListDeleteMutation, + useListMuteMutation, + useListQuery, +} from '#/state/queries/list' import {FeedDescriptor} from '#/state/queries/post-feed' +import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' +import { + usePinFeedMutation, + usePreferencesQuery, + useSetSaveFeedsMutation, + useUnpinFeedMutation, +} from '#/state/queries/preferences' +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 {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 {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 {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useSetMinimalShellMode} from '#/state/shell' -import {useModalControls} from '#/state/modals' -import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' -import {useResolveUriQuery} from '#/state/queries/resolve-uri' -import { - useListQuery, - useListMuteMutation, - useListBlockMutation, - useListDeleteMutation, -} 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 { - usePreferencesQuery, - usePinFeedMutation, - useUnpinFeedMutation, - useSetSaveFeedsMutation, -} from '#/state/queries/preferences' -import {logger} from '#/logger' -import {useAnalytics} from '#/lib/analytics/analytics' -import {listenSoftReset} from '#/state/events' +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 251c7063..0003dbd5 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 830a73ff..8a7fa5e7 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() 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) { - {/* TODO ( - <> - - Invite a Friend - - - - 0 ? primaryBg : pal.btn, - ]}> - 0 - ? primaryText - : pal.text) as FontAwesomeIconStyle - } - /> - - 0 ? pal.link : pal.text}> - {invites?.disabled ? ( - - Your invite codes are hidden when logged in using an App - Password - - ) : invitesAvailable === 1 ? ( - {invitesAvailable} invite code available - ) : ( - {invitesAvailable} invite codes available - )} - - - - - - )*/} - Accessibility @@ -738,6 +681,19 @@ export function SettingsScreen({}: Props) { /> )} + {isNative && ( + + setHapticsDisabled(!isHapticsDisabled)} + /> + + )} Account diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index f41631a9..c35fa106 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 ( <>