From 7b413d05879fae18dc166a0dbb3fa7f4f4e6c249 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 11 Apr 2024 10:09:09 -0700 Subject: [PATCH] improve android haptics, offer toggle for haptics --- patches/expo-haptics+12.8.1.md | 11 ++ patches/expo-haptics+12.8.1.patch | 13 +++ src/lib/haptics.ts | 20 ++-- .../Profile/Header/ProfileHeaderLabeler.tsx | 6 +- 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 | 23 ++-- src/view/screens/ProfileFeed.tsx | 39 +++++-- src/view/screens/ProfileList.tsx | 107 +++++++++--------- src/view/screens/SavedFeeds.tsx | 53 +++++---- src/view/screens/Settings/index.tsx | 84 ++++---------- src/view/shell/bottom-bar/BottomBar.tsx | 6 +- 14 files changed, 252 insertions(+), 168 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 0000000000..afa7395bc0 --- /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 0000000000..a95b56f3be --- /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 b22d69d703..f59493d447 100644 --- a/src/lib/haptics.ts +++ b/src/lib/haptics.ts @@ -13,26 +13,30 @@ const hapticImpact: ImpactFeedbackStyle = isIOS : ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s export class Haptics { - static default() { - if (isWeb) { + static default(enabled: boolean) { + if (!enabled || isWeb) { return } + impactAsync(hapticImpact) } - static impact(type: ImpactFeedbackStyle = hapticImpact) { - if (isWeb) { + static impact(type: ImpactFeedbackStyle = hapticImpact, enabled: boolean) { + if (!enabled || isWeb) { return } impactAsync(type) } - static selection() { - if (isWeb) { + static selection(enabled: boolean) { + if (!enabled || isWeb) { return } selectionAsync() } - static notification = (type: 'success' | 'warning' | 'error') => { - if (isWeb) { + static notification = ( + type: 'success' | 'warning' | 'error', + enabled: boolean, + ) => { + if (!enabled || isWeb) { return } switch (type) { diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 4d8dbad86c..ec3ada6708 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -22,6 +22,7 @@ import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' import {useAnalytics} from 'lib/analytics/analytics' import {useProfileShadow} from 'state/cache/profile-shadow' +import {useHapticsDisabled} from 'state/preferences/disable-haptics' import {ProfileMenu} from '#/view/com/profile/ProfileMenu' import * as Toast from '#/view/com/util/Toast' import {atoms as a, tokens, useTheme} from '#/alf' @@ -64,6 +65,7 @@ let ProfileHeaderLabeler = ({ const {currentAccount, hasSession} = useSession() const {openModal} = useModalControls() const {track} = useAnalytics() + const isHapticsDisabled = useHapticsDisabled() const cantSubscribePrompt = Prompt.usePromptControl() const isSelf = currentAccount?.did === profile.did @@ -93,7 +95,7 @@ let ProfileHeaderLabeler = ({ return } try { - Haptics.default() + Haptics.default(isHapticsDisabled) if (likeUri) { await unlikeMod({uri: likeUri}) @@ -114,7 +116,7 @@ let ProfileHeaderLabeler = ({ ) logger.error(`Failed to toggle labeler like`, {message: e.message}) } - }, [labeler, likeUri, likeMod, unlikeMod, track, _]) + }, [labeler, isHapticsDisabled, 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 fd94a96a24..ca7967cd2e 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 0aefaa4744..190cc80b03 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: true, } diff --git a/src/state/preferences/disable-haptics.tsx b/src/state/preferences/disable-haptics.tsx new file mode 100644 index 0000000000..af2c55a182 --- /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 cf1d901511..804d0fc310 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 58874cd551..68ea1e22c5 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -32,6 +32,7 @@ import { } from '#/state/queries/post' import {useRequireAuth} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' +import {useHapticsDisabled} from 'state/preferences/disable-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 +68,7 @@ let PostCtrls = ({ ) const requireAuth = useRequireAuth() const loggedOutWarningPromptControl = useDialogControl() + const isHapticsDisabled = useHapticsDisabled() const shouldShowLoggedOutWarning = React.useMemo(() => { return !!post.author.labels?.find( @@ -84,7 +86,7 @@ let PostCtrls = ({ const onPressToggleLike = React.useCallback(async () => { try { if (!post.viewer?.like) { - Haptics.default() + Haptics.default(isHapticsDisabled) await queueLike() } else { await queueUnlike() @@ -94,13 +96,13 @@ let PostCtrls = ({ throw e } } - }, [post.viewer?.like, queueLike, queueUnlike]) + }, [isHapticsDisabled, post.viewer?.like, queueLike, queueUnlike]) const onRepost = useCallback(async () => { closeModal() try { if (!post.viewer?.repost) { - Haptics.default() + Haptics.default(isHapticsDisabled) await queueRepost() } else { await queueUnrepost() @@ -110,7 +112,13 @@ let PostCtrls = ({ throw e } } - }, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal]) + }, [ + closeModal, + post.viewer?.repost, + isHapticsDisabled, + queueRepost, + queueUnrepost, + ]) const onQuote = useCallback(() => { closeModal() @@ -123,15 +131,16 @@ let PostCtrls = ({ indexedAt: post.indexedAt, }, }) - Haptics.default() + Haptics.default(isHapticsDisabled) }, [ + closeModal, + openComposer, post.uri, post.cid, post.author, post.indexedAt, record.text, - openComposer, - closeModal, + isHapticsDisabled, ]) const onShare = useCallback(() => { diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 4560e14ebc..d50a64e6bb 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -39,6 +39,7 @@ import {pluralize} from 'lib/strings/helpers' import {makeRecordUri} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers' import {s} from 'lib/styles' +import {useHapticsDisabled} from 'state/preferences/disable-haptics' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {Feed} from 'view/com/posts/Feed' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' @@ -159,6 +160,7 @@ export function ProfileFeedScreenInner({ const reportDialogControl = useReportDialogControl() const {openComposer} = useComposerControls() const {track} = useAnalytics() + const isHapticsDisabled = useHapticsDisabled() const feedSectionRef = React.useRef(null) const isScreenFocused = useIsFocused() @@ -201,7 +203,7 @@ export function ProfileFeedScreenInner({ const onToggleSaved = React.useCallback(async () => { try { - Haptics.default() + Haptics.default(isHapticsDisabled) if (isSaved) { await removeFeed({uri: feedInfo.uri}) @@ -221,18 +223,19 @@ export function ProfileFeedScreenInner({ logger.error('Failed up update feeds', {message: err}) } }, [ - feedInfo, + isHapticsDisabled, isSaved, - saveFeed, removeFeed, - resetSaveFeed, + feedInfo.uri, resetRemoveFeed, _, + saveFeed, + resetSaveFeed, ]) const onTogglePinned = React.useCallback(async () => { try { - Haptics.default() + Haptics.default(isHapticsDisabled) if (isPinned) { await unpinFeed({uri: feedInfo.uri}) @@ -245,7 +248,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, _]) + }, [ + isHapticsDisabled, + isPinned, + unpinFeed, + feedInfo.uri, + resetUnpinFeed, + pinFeed, + resetPinFeed, + _, + ]) const onPressShare = React.useCallback(() => { const url = toShareUrl(feedInfo.route.href) @@ -517,6 +529,7 @@ function AboutSection({ const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) const {hasSession} = useSession() const {track} = useAnalytics() + const isHapticsDisabled = useHapticsDisabled() const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = useUnlikeMutation() @@ -527,7 +540,7 @@ function AboutSection({ const onToggleLiked = React.useCallback(async () => { try { - Haptics.default() + Haptics.default(isHapticsDisabled) if (isLiked && likeUri) { await unlikeFeed({uri: likeUri}) @@ -546,7 +559,17 @@ function AboutSection({ ) logger.error('Failed up toggle like', {message: err}) } - }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) + }, [ + isHapticsDisabled, + isLiked, + likeUri, + unlikeFeed, + track, + likeFeed, + feedInfo.uri, + feedInfo.cid, + _, + ]) return ( diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 58b89f2399..c361acdbdd 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -1,69 +1,71 @@ 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 {Haptics} 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 {useHapticsDisabled} from 'state/preferences/disable-haptics' +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 +256,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const {data: preferences} = usePreferencesQuery() const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {track} = useAnalytics() + const isHapticsDisabled = useHapticsDisabled() const deleteListPromptControl = useDialogControl() const subscribeMutePromptControl = useDialogControl() @@ -263,7 +266,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() + Haptics.default(isHapticsDisabled) try { if (isPinned) { @@ -275,7 +278,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, _]) + }, [isHapticsDisabled, 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 251c706384..54e73ddede 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -1,31 +1,33 @@ 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 {Haptics} 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 {useHapticsDisabled} from 'state/preferences/disable-haptics' +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 +191,14 @@ function ListItem({ }) { const pal = usePalette('default') const {_} = useLingui() + const isHapticsDisabled = useHapticsDisabled() const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() const {isPending: isUnpinPending, mutateAsync: unpinFeed} = useUnpinFeedMutation() const isPending = isPinPending || isUnpinPending const onTogglePinned = React.useCallback(async () => { - Haptics.default() + Haptics.default(isHapticsDisabled) try { resetSaveFeedsMutationState() @@ -209,7 +212,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, _]) + }, [ + isHapticsDisabled, + 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 830a73ff26..8a7fa5e714 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 f41631a969..1e889f7721 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -32,6 +32,7 @@ import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useShellLayout} from '#/state/shell/shell-layout' import {useCloseAllActiveElements} from '#/state/util' +import {useHapticsDisabled} from 'state/preferences/disable-haptics' import {Button} from '#/view/com/util/forms/Button' import {Text} from '#/view/com/util/text/Text' import {UserAvatar} from '#/view/com/util/UserAvatar' @@ -59,6 +60,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { const closeAllActiveElements = useCloseAllActiveElements() const dedupe = useDedupe() const accountSwitchControl = useDialogControl() + const isHapticsDisabled = useHapticsDisabled() const showSignIn = React.useCallback(() => { closeAllActiveElements() @@ -104,9 +106,9 @@ export function BottomBar({navigation}: BottomTabBarProps) { }, [onPressTab]) const onLongPressProfile = React.useCallback(() => { - Haptics.default() + Haptics.default(isHapticsDisabled) accountSwitchControl.open() - }, [accountSwitchControl]) + }, [accountSwitchControl, isHapticsDisabled]) return ( <>