Bluesky app fork with some witchin' additions 💫

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

authored by hailey.at and committed by

GitHub 740cd029 9007810c

+231 -200
+11
patches/expo-haptics+12.8.1.md
··· 1 + # Expo Haptics Patch 2 + 3 + Whenever we migrated to Expo Haptics, there was a difference between how the previous and new libraries handled the 4 + Android implementation of an iOS "light" haptic. The previous library used the `Vibration` API solely, which does not 5 + have any configuration for intensity of vibration. The `Vibration` API has also been deprecated since SDK 26. See: 6 + https://github.com/mkuczera/react-native-haptic-feedback/blob/master/android/src/main/java/com/mkuczera/vibrateFactory/VibrateWithDuration.java 7 + 8 + 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 9 + The timing and intensity of their haptic configurations though differs greatly from the original implementation. This 10 + patch uses the new `VibrationManager` API to create the same vibration that would have been seen in the deprecated 11 + `Vibration` API.
+13
patches/expo-haptics+12.8.1.patch
··· 1 + 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 2 + index 26c52af..b949a4c 100644 3 + --- a/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt 4 + +++ b/node_modules/expo-haptics/android/src/main/java/expo/modules/haptics/HapticsModule.kt 5 + @@ -42,7 +42,7 @@ class HapticsModule : Module() { 6 + 7 + private fun vibrate(type: HapticsVibrationType) { 8 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 9 + - vibrator.vibrate(VibrationEffect.createWaveform(type.timings, type.amplitudes, -1)) 10 + + vibrator.vibrate(VibrationEffect.createWaveform(type.oldSDKPattern, intArrayOf(0, 100), -1)) 11 + } else { 12 + @Suppress("DEPRECATION") 13 + vibrator.vibrate(type.oldSDKPattern, -1)
+9 -36
src/lib/haptics.ts
··· 1 - import { 2 - impactAsync, 3 - ImpactFeedbackStyle, 4 - notificationAsync, 5 - NotificationFeedbackType, 6 - selectionAsync, 7 - } from 'expo-haptics' 1 + import React from 'react' 2 + import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' 8 3 9 4 import {isIOS, isWeb} from 'platform/detection' 5 + import {useHapticsDisabled} from 'state/preferences/disable-haptics' 10 6 11 7 const hapticImpact: ImpactFeedbackStyle = isIOS 12 8 ? ImpactFeedbackStyle.Medium 13 9 : ImpactFeedbackStyle.Light // Users said the medium impact was too strong on Android; see APP-537s 14 10 15 - export class Haptics { 16 - static default() { 17 - if (isWeb) { 11 + export function useHaptics() { 12 + const isHapticsDisabled = useHapticsDisabled() 13 + 14 + return React.useCallback(() => { 15 + if (isHapticsDisabled || isWeb) { 18 16 return 19 17 } 20 18 impactAsync(hapticImpact) 21 - } 22 - static impact(type: ImpactFeedbackStyle = hapticImpact) { 23 - if (isWeb) { 24 - return 25 - } 26 - impactAsync(type) 27 - } 28 - static selection() { 29 - if (isWeb) { 30 - return 31 - } 32 - selectionAsync() 33 - } 34 - static notification = (type: 'success' | 'warning' | 'error') => { 35 - if (isWeb) { 36 - return 37 - } 38 - switch (type) { 39 - case 'success': 40 - return notificationAsync(NotificationFeedbackType.Success) 41 - case 'warning': 42 - return notificationAsync(NotificationFeedbackType.Warning) 43 - case 'error': 44 - return notificationAsync(NotificationFeedbackType.Error) 45 - } 46 - } 19 + }, [isHapticsDisabled]) 47 20 }
+4 -3
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 10 10 import {msg, Trans} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 12 13 - import {Haptics} from '#/lib/haptics' 14 13 import {isAppLabeler} from '#/lib/moderation' 15 14 import {pluralize} from '#/lib/strings/helpers' 16 15 import {logger} from '#/logger' ··· 21 20 import {usePreferencesQuery} from '#/state/queries/preferences' 22 21 import {useSession} from '#/state/session' 23 22 import {useAnalytics} from 'lib/analytics/analytics' 23 + import {useHaptics} from 'lib/haptics' 24 24 import {useProfileShadow} from 'state/cache/profile-shadow' 25 25 import {ProfileMenu} from '#/view/com/profile/ProfileMenu' 26 26 import * as Toast from '#/view/com/util/Toast' ··· 64 64 const {currentAccount, hasSession} = useSession() 65 65 const {openModal} = useModalControls() 66 66 const {track} = useAnalytics() 67 + const playHaptic = useHaptics() 67 68 const cantSubscribePrompt = Prompt.usePromptControl() 68 69 const isSelf = currentAccount?.did === profile.did 69 70 ··· 93 94 return 94 95 } 95 96 try { 96 - Haptics.default() 97 + playHaptic() 97 98 98 99 if (likeUri) { 99 100 await unlikeMod({uri: likeUri}) ··· 114 115 ) 115 116 logger.error(`Failed to toggle labeler like`, {message: e.message}) 116 117 } 117 - }, [labeler, likeUri, likeMod, unlikeMod, track, _]) 118 + }, [labeler, playHaptic, likeUri, unlikeMod, track, likeMod, _]) 118 119 119 120 const onPressEditProfile = React.useCallback(() => { 120 121 track('ProfileHeader:EditProfileButtonClicked')
+2 -1
src/state/persisted/legacy.ts
··· 2 2 3 3 import {logger} from '#/logger' 4 4 import {defaults, Schema, schema} from '#/state/persisted/schema' 5 - import {write, read} from '#/state/persisted/store' 5 + import {read, write} from '#/state/persisted/store' 6 6 7 7 /** 8 8 * The shape of the serialized data from our legacy Mobx store. ··· 113 113 externalEmbeds: defaults.externalEmbeds, 114 114 lastSelectedHomeFeed: defaults.lastSelectedHomeFeed, 115 115 pdsAddressHistory: defaults.pdsAddressHistory, 116 + disableHaptics: defaults.disableHaptics, 116 117 } 117 118 } 118 119
+3
src/state/persisted/schema.ts
··· 1 1 import {z} from 'zod' 2 + 2 3 import {deviceLocales} from '#/platform/detection' 3 4 4 5 const externalEmbedOptions = ['show', 'hide'] as const ··· 58 59 useInAppBrowser: z.boolean().optional(), 59 60 lastSelectedHomeFeed: z.string().optional(), 60 61 pdsAddressHistory: z.array(z.string()).optional(), 62 + disableHaptics: z.boolean().optional(), 61 63 }) 62 64 export type Schema = z.infer<typeof schema> 63 65 ··· 93 95 useInAppBrowser: undefined, 94 96 lastSelectedHomeFeed: undefined, 95 97 pdsAddressHistory: [], 98 + disableHaptics: false, 96 99 }
+42
src/state/preferences/disable-haptics.tsx
··· 1 + import React from 'react' 2 + 3 + import * as persisted from '#/state/persisted' 4 + 5 + type StateContext = boolean 6 + type SetContext = (v: boolean) => void 7 + 8 + const stateContext = React.createContext<StateContext>( 9 + Boolean(persisted.defaults.disableHaptics), 10 + ) 11 + const setContext = React.createContext<SetContext>((_: boolean) => {}) 12 + 13 + export function Provider({children}: {children: React.ReactNode}) { 14 + const [state, setState] = React.useState( 15 + Boolean(persisted.get('disableHaptics')), 16 + ) 17 + 18 + const setStateWrapped = React.useCallback( 19 + (hapticsEnabled: persisted.Schema['disableHaptics']) => { 20 + setState(Boolean(hapticsEnabled)) 21 + persisted.write('disableHaptics', hapticsEnabled) 22 + }, 23 + [setState], 24 + ) 25 + 26 + React.useEffect(() => { 27 + return persisted.onUpdate(() => { 28 + setState(Boolean(persisted.get('disableHaptics'))) 29 + }) 30 + }, [setStateWrapped]) 31 + 32 + return ( 33 + <stateContext.Provider value={state}> 34 + <setContext.Provider value={setStateWrapped}> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + ) 39 + } 40 + 41 + export const useHapticsDisabled = () => React.useContext(stateContext) 42 + export const useSetHapticsDisabled = () => React.useContext(setContext)
+7 -3
src/state/preferences/index.tsx
··· 1 1 import React from 'react' 2 - import {Provider as LanguagesProvider} from './languages' 2 + 3 3 import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required' 4 4 import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts' 5 + import {Provider as DisableHapticsProvider} from './disable-haptics' 5 6 import {Provider as ExternalEmbedsProvider} from './external-embeds-prefs' 6 7 import {Provider as InAppBrowserProvider} from './in-app-browser' 8 + import {Provider as LanguagesProvider} from './languages' 7 9 8 - export {useLanguagePrefs, useLanguagePrefsApi} from './languages' 9 10 export { 10 11 useRequireAltTextEnabled, 11 12 useSetRequireAltTextEnabled, ··· 16 17 } from './external-embeds-prefs' 17 18 export * from './hidden-posts' 18 19 export {useLabelDefinitions} from './label-defs' 20 + export {useLanguagePrefs, useLanguagePrefsApi} from './languages' 19 21 20 22 export function Provider({children}: React.PropsWithChildren<{}>) { 21 23 return ( ··· 23 25 <AltTextRequiredProvider> 24 26 <ExternalEmbedsProvider> 25 27 <HiddenPostsProvider> 26 - <InAppBrowserProvider>{children}</InAppBrowserProvider> 28 + <InAppBrowserProvider> 29 + <DisableHapticsProvider>{children}</DisableHapticsProvider> 30 + </InAppBrowserProvider> 27 31 </HiddenPostsProvider> 28 32 </ExternalEmbedsProvider> 29 33 </AltTextRequiredProvider>
+10 -8
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 16 16 import {useLingui} from '@lingui/react' 17 17 18 18 import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' 19 - import {Haptics} from '#/lib/haptics' 20 19 import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons' 21 20 import {makeProfileLink} from '#/lib/routes/links' 22 21 import {shareUrl} from '#/lib/sharing' ··· 32 31 } from '#/state/queries/post' 33 32 import {useRequireAuth} from '#/state/session' 34 33 import {useComposerControls} from '#/state/shell/composer' 34 + import {useHaptics} from 'lib/haptics' 35 35 import {useDialogControl} from '#/components/Dialog' 36 36 import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox' 37 37 import * as Prompt from '#/components/Prompt' ··· 67 67 ) 68 68 const requireAuth = useRequireAuth() 69 69 const loggedOutWarningPromptControl = useDialogControl() 70 + const playHaptic = useHaptics() 70 71 71 72 const shouldShowLoggedOutWarning = React.useMemo(() => { 72 73 return !!post.author.labels?.find( ··· 84 85 const onPressToggleLike = React.useCallback(async () => { 85 86 try { 86 87 if (!post.viewer?.like) { 87 - Haptics.default() 88 + playHaptic() 88 89 await queueLike() 89 90 } else { 90 91 await queueUnlike() ··· 94 95 throw e 95 96 } 96 97 } 97 - }, [post.viewer?.like, queueLike, queueUnlike]) 98 + }, [playHaptic, post.viewer?.like, queueLike, queueUnlike]) 98 99 99 100 const onRepost = useCallback(async () => { 100 101 closeModal() 101 102 try { 102 103 if (!post.viewer?.repost) { 103 - Haptics.default() 104 + playHaptic() 104 105 await queueRepost() 105 106 } else { 106 107 await queueUnrepost() ··· 110 111 throw e 111 112 } 112 113 } 113 - }, [post.viewer?.repost, queueRepost, queueUnrepost, closeModal]) 114 + }, [closeModal, post.viewer?.repost, playHaptic, queueRepost, queueUnrepost]) 114 115 115 116 const onQuote = useCallback(() => { 116 117 closeModal() ··· 123 124 indexedAt: post.indexedAt, 124 125 }, 125 126 }) 126 - Haptics.default() 127 + playHaptic() 127 128 }, [ 129 + closeModal, 130 + openComposer, 128 131 post.uri, 129 132 post.cid, 130 133 post.author, 131 134 post.indexedAt, 132 135 record.text, 133 - openComposer, 134 - closeModal, 136 + playHaptic, 135 137 ]) 136 138 137 139 const onShare = useCallback(() => {
+21 -9
src/view/screens/ProfileFeed.tsx
··· 27 27 import {useSession} from '#/state/session' 28 28 import {useComposerControls} from '#/state/shell/composer' 29 29 import {useAnalytics} from 'lib/analytics/analytics' 30 - import {Haptics} from 'lib/haptics' 30 + import {useHaptics} from 'lib/haptics' 31 31 import {usePalette} from 'lib/hooks/usePalette' 32 32 import {useSetTitle} from 'lib/hooks/useSetTitle' 33 33 import {ComposeIcon2} from 'lib/icons' ··· 159 159 const reportDialogControl = useReportDialogControl() 160 160 const {openComposer} = useComposerControls() 161 161 const {track} = useAnalytics() 162 + const playHaptic = useHaptics() 162 163 const feedSectionRef = React.useRef<SectionRef>(null) 163 164 const isScreenFocused = useIsFocused() 164 165 ··· 201 202 202 203 const onToggleSaved = React.useCallback(async () => { 203 204 try { 204 - Haptics.default() 205 + playHaptic() 205 206 206 207 if (isSaved) { 207 208 await removeFeed({uri: feedInfo.uri}) ··· 221 222 logger.error('Failed up update feeds', {message: err}) 222 223 } 223 224 }, [ 224 - feedInfo, 225 + playHaptic, 225 226 isSaved, 226 - saveFeed, 227 227 removeFeed, 228 - resetSaveFeed, 228 + feedInfo, 229 229 resetRemoveFeed, 230 230 _, 231 + saveFeed, 232 + resetSaveFeed, 231 233 ]) 232 234 233 235 const onTogglePinned = React.useCallback(async () => { 234 236 try { 235 - Haptics.default() 237 + playHaptic() 236 238 237 239 if (isPinned) { 238 240 await unpinFeed({uri: feedInfo.uri}) ··· 245 247 Toast.show(_(msg`There was an issue contacting the server`)) 246 248 logger.error('Failed to toggle pinned feed', {message: e}) 247 249 } 248 - }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _]) 250 + }, [ 251 + playHaptic, 252 + isPinned, 253 + unpinFeed, 254 + feedInfo, 255 + resetUnpinFeed, 256 + pinFeed, 257 + resetPinFeed, 258 + _, 259 + ]) 249 260 250 261 const onPressShare = React.useCallback(() => { 251 262 const url = toShareUrl(feedInfo.route.href) ··· 517 528 const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) 518 529 const {hasSession} = useSession() 519 530 const {track} = useAnalytics() 531 + const playHaptic = useHaptics() 520 532 const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() 521 533 const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = 522 534 useUnlikeMutation() ··· 527 539 528 540 const onToggleLiked = React.useCallback(async () => { 529 541 try { 530 - Haptics.default() 542 + playHaptic() 531 543 532 544 if (isLiked && likeUri) { 533 545 await unlikeFeed({uri: likeUri}) ··· 546 558 ) 547 559 logger.error('Failed up toggle like', {message: err}) 548 560 } 549 - }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) 561 + }, [playHaptic, isLiked, likeUri, unlikeFeed, track, likeFeed, feedInfo, _]) 550 562 551 563 return ( 552 564 <View style={[styles.aboutSectionContainer]}>
+54 -52
src/view/screens/ProfileList.tsx
··· 1 1 import React, {useCallback, useMemo} from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 + import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' 4 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 + import {msg, Trans} from '@lingui/macro' 6 + import {useLingui} from '@lingui/react' 3 7 import {useFocusEffect, useIsFocused} from '@react-navigation/native' 4 - import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' 5 8 import {useNavigation} from '@react-navigation/native' 6 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 7 - import {AppBskyGraphDefs, AtUri, RichText as RichTextAPI} from '@atproto/api' 8 9 import {useQueryClient} from '@tanstack/react-query' 9 - import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 10 - import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' 11 - import {Feed} from 'view/com/posts/Feed' 12 - import {Text} from 'view/com/util/text/Text' 13 - import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown' 14 - import {CenteredView} from 'view/com/util/Views' 15 - import {EmptyState} from 'view/com/util/EmptyState' 16 - import {LoadingScreen} from 'view/com/util/LoadingScreen' 17 - import {RichText} from '#/components/RichText' 18 - import {Button} from 'view/com/util/forms/Button' 19 - import {TextLink} from 'view/com/util/Link' 20 - import {ListRef} from 'view/com/util/List' 21 - import * as Toast from 'view/com/util/Toast' 22 - import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 23 - import {FAB} from 'view/com/util/fab/FAB' 24 - import {Haptics} from 'lib/haptics' 25 - import {FeedDescriptor} from '#/state/queries/post-feed' 26 - import {usePalette} from 'lib/hooks/usePalette' 27 - import {useSetTitle} from 'lib/hooks/useSetTitle' 28 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 29 - import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 30 - import {NavigationProp} from 'lib/routes/types' 31 - import {toShareUrl} from 'lib/strings/url-helpers' 32 - import {shareUrl} from 'lib/sharing' 33 - import {s} from 'lib/styles' 34 - import {sanitizeHandle} from 'lib/strings/handles' 35 - import {makeProfileLink, makeListLink} from 'lib/routes/links' 36 - import {ComposeIcon2} from 'lib/icons' 37 - import {ListMembers} from '#/view/com/lists/ListMembers' 38 - import {Trans, msg} from '@lingui/macro' 39 - import {useLingui} from '@lingui/react' 40 - import {useSetMinimalShellMode} from '#/state/shell' 10 + 11 + import {useAnalytics} from '#/lib/analytics/analytics' 12 + import {cleanError} from '#/lib/strings/errors' 13 + import {logger} from '#/logger' 14 + import {isNative, isWeb} from '#/platform/detection' 15 + import {listenSoftReset} from '#/state/events' 41 16 import {useModalControls} from '#/state/modals' 42 - import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 43 - import {useResolveUriQuery} from '#/state/queries/resolve-uri' 44 17 import { 45 - useListQuery, 46 - useListMuteMutation, 47 18 useListBlockMutation, 48 19 useListDeleteMutation, 20 + useListMuteMutation, 21 + useListQuery, 49 22 } from '#/state/queries/list' 50 - import {cleanError} from '#/lib/strings/errors' 51 - import {useSession} from '#/state/session' 52 - import {useComposerControls} from '#/state/shell/composer' 53 - import {isNative, isWeb} from '#/platform/detection' 54 - import {truncateAndInvalidate} from '#/state/queries/util' 23 + import {FeedDescriptor} from '#/state/queries/post-feed' 24 + import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' 55 25 import { 56 - usePreferencesQuery, 57 26 usePinFeedMutation, 58 - useUnpinFeedMutation, 27 + usePreferencesQuery, 59 28 useSetSaveFeedsMutation, 29 + useUnpinFeedMutation, 60 30 } from '#/state/queries/preferences' 61 - import {logger} from '#/logger' 62 - import {useAnalytics} from '#/lib/analytics/analytics' 63 - import {listenSoftReset} from '#/state/events' 31 + import {useResolveUriQuery} from '#/state/queries/resolve-uri' 32 + import {truncateAndInvalidate} from '#/state/queries/util' 33 + import {useSession} from '#/state/session' 34 + import {useSetMinimalShellMode} from '#/state/shell' 35 + import {useComposerControls} from '#/state/shell/composer' 36 + import {useHaptics} from 'lib/haptics' 37 + import {usePalette} from 'lib/hooks/usePalette' 38 + import {useSetTitle} from 'lib/hooks/useSetTitle' 39 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 40 + import {ComposeIcon2} from 'lib/icons' 41 + import {makeListLink, makeProfileLink} from 'lib/routes/links' 42 + import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' 43 + import {NavigationProp} from 'lib/routes/types' 44 + import {shareUrl} from 'lib/sharing' 45 + import {sanitizeHandle} from 'lib/strings/handles' 46 + import {toShareUrl} from 'lib/strings/url-helpers' 47 + import {s} from 'lib/styles' 48 + import {ListMembers} from '#/view/com/lists/ListMembers' 49 + import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 50 + import {Feed} from 'view/com/posts/Feed' 51 + import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' 52 + import {EmptyState} from 'view/com/util/EmptyState' 53 + import {FAB} from 'view/com/util/fab/FAB' 54 + import {Button} from 'view/com/util/forms/Button' 55 + import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown' 56 + import {TextLink} from 'view/com/util/Link' 57 + import {ListRef} from 'view/com/util/List' 58 + import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 59 + import {LoadingScreen} from 'view/com/util/LoadingScreen' 60 + import {Text} from 'view/com/util/text/Text' 61 + import * as Toast from 'view/com/util/Toast' 62 + import {CenteredView} from 'view/com/util/Views' 64 63 import {atoms as a, useTheme} from '#/alf' 65 - import * as Prompt from '#/components/Prompt' 66 64 import {useDialogControl} from '#/components/Dialog' 65 + import * as Prompt from '#/components/Prompt' 66 + import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' 67 + import {RichText} from '#/components/RichText' 67 68 68 69 const SECTION_TITLES_CURATE = ['Posts', 'About'] 69 70 const SECTION_TITLES_MOD = ['About'] ··· 254 255 const {data: preferences} = usePreferencesQuery() 255 256 const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() 256 257 const {track} = useAnalytics() 258 + const playHaptic = useHaptics() 257 259 258 260 const deleteListPromptControl = useDialogControl() 259 261 const subscribeMutePromptControl = useDialogControl() ··· 263 265 const isSaved = preferences?.feeds?.saved?.includes(list.uri) 264 266 265 267 const onTogglePinned = React.useCallback(async () => { 266 - Haptics.default() 268 + playHaptic() 267 269 268 270 try { 269 271 if (isPinned) { ··· 275 277 Toast.show(_(msg`There was an issue contacting the server`)) 276 278 logger.error('Failed to toggle pinned feed', {message: e}) 277 279 } 278 - }, [list.uri, isPinned, pinFeed, unpinFeed, _]) 280 + }, [playHaptic, isPinned, unpinFeed, list.uri, pinFeed, _]) 279 281 280 282 const onSubscribeMute = useCallback(async () => { 281 283 try {
+31 -21
src/view/screens/SavedFeeds.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View, ActivityIndicator, Pressable} from 'react-native' 2 + import {ActivityIndicator, Pressable, StyleSheet, View} from 'react-native' 3 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 3 6 import {useFocusEffect} from '@react-navigation/native' 4 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' 8 + 5 9 import {track} from '#/lib/analytics/analytics' 10 + import {logger} from '#/logger' 11 + import { 12 + usePinFeedMutation, 13 + usePreferencesQuery, 14 + useSetSaveFeedsMutation, 15 + useUnpinFeedMutation, 16 + } from '#/state/queries/preferences' 17 + import {useSetMinimalShellMode} from '#/state/shell' 6 18 import {useAnalytics} from 'lib/analytics/analytics' 19 + import {useHaptics} from 'lib/haptics' 7 20 import {usePalette} from 'lib/hooks/usePalette' 21 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 8 22 import {CommonNavigatorParams} from 'lib/routes/types' 9 - import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 10 - import {ViewHeader} from 'view/com/util/ViewHeader' 11 - import {ScrollView, CenteredView} from 'view/com/util/Views' 23 + import {colors, s} from 'lib/styles' 24 + import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 25 + import {TextLink} from 'view/com/util/Link' 12 26 import {Text} from 'view/com/util/text/Text' 13 - import {s, colors} from 'lib/styles' 14 - import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' 15 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 16 27 import * as Toast from 'view/com/util/Toast' 17 - import {Haptics} from 'lib/haptics' 18 - import {TextLink} from 'view/com/util/Link' 19 - import {logger} from '#/logger' 20 - import {useSetMinimalShellMode} from '#/state/shell' 21 - import {Trans, msg} from '@lingui/macro' 22 - import {useLingui} from '@lingui/react' 23 - import { 24 - usePreferencesQuery, 25 - usePinFeedMutation, 26 - useUnpinFeedMutation, 27 - useSetSaveFeedsMutation, 28 - } from '#/state/queries/preferences' 28 + import {ViewHeader} from 'view/com/util/ViewHeader' 29 + import {CenteredView, ScrollView} from 'view/com/util/Views' 29 30 30 31 const HITSLOP_TOP = { 31 32 top: 20, ··· 189 190 }) { 190 191 const pal = usePalette('default') 191 192 const {_} = useLingui() 193 + const playHaptic = useHaptics() 192 194 const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() 193 195 const {isPending: isUnpinPending, mutateAsync: unpinFeed} = 194 196 useUnpinFeedMutation() 195 197 const isPending = isPinPending || isUnpinPending 196 198 197 199 const onTogglePinned = React.useCallback(async () => { 198 - Haptics.default() 200 + playHaptic() 199 201 200 202 try { 201 203 resetSaveFeedsMutationState() ··· 209 211 Toast.show(_(msg`There was an issue contacting the server`)) 210 212 logger.error('Failed to toggle pinned feed', {message: e}) 211 213 } 212 - }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _]) 214 + }, [ 215 + playHaptic, 216 + resetSaveFeedsMutationState, 217 + isPinned, 218 + unpinFeed, 219 + feedUri, 220 + pinFeed, 221 + _, 222 + ]) 213 223 214 224 const onPressUp = React.useCallback(async () => { 215 225 if (!isPinned) return
+20 -64
src/view/screens/Settings/index.tsx
··· 20 20 import {useFocusEffect, useNavigation} from '@react-navigation/native' 21 21 import {useQueryClient} from '@tanstack/react-query' 22 22 23 - import {isNative} from '#/platform/detection' 23 + import {isIOS, isNative} from '#/platform/detection' 24 24 import {useModalControls} from '#/state/modals' 25 25 import {clearLegacyStorage} from '#/state/persisted/legacy' 26 - // TODO import {useInviteCodesQuery} from '#/state/queries/invites' 27 26 import {clear as clearStorage} from '#/state/persisted/store' 28 27 import { 29 28 useRequireAltTextEnabled, ··· 57 56 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' 58 57 import {NavigationProp} from 'lib/routes/types' 59 58 import {colors, s} from 'lib/styles' 59 + import { 60 + useHapticsDisabled, 61 + useSetHapticsDisabled, 62 + } from 'state/preferences/disable-haptics' 60 63 import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' 61 64 import {SelectableBtn} from 'view/com/util/forms/SelectableBtn' 62 65 import {ToggleButton} from 'view/com/util/forms/ToggleButton' ··· 155 158 const setRequireAltTextEnabled = useSetRequireAltTextEnabled() 156 159 const inAppBrowserPref = useInAppBrowser() 157 160 const setUseInAppBrowser = useSetInAppBrowser() 161 + const isHapticsDisabled = useHapticsDisabled() 162 + const setHapticsDisabled = useSetHapticsDisabled() 158 163 const onboardingDispatch = useOnboardingDispatch() 159 164 const navigation = useNavigation<NavigationProp>() 160 165 const {isMobile} = useWebMediaQueries() ··· 162 167 const {openModal} = useModalControls() 163 168 const {isSwitchingAccounts, accounts, currentAccount} = useSession() 164 169 const {mutate: clearPreferences} = useClearPreferencesMutation() 165 - // TODO 166 - // const {data: invites} = useInviteCodesQuery() 167 - // const invitesAvailable = invites?.available?.length ?? 0 168 170 const {setShowLoggedOut} = useLoggedOutViewControls() 169 171 const closeAllActiveElements = useCloseAllActiveElements() 170 172 const exportCarControl = useDialogControl() ··· 219 221 const onPressExportRepository = React.useCallback(() => { 220 222 exportCarControl.open() 221 223 }, [exportCarControl]) 222 - 223 - /* TODO 224 - const onPressInviteCodes = React.useCallback(() => { 225 - track('Settings:InvitecodesButtonClicked') 226 - openModal({name: 'invite-codes'}) 227 - }, [track, openModal]) 228 - */ 229 224 230 225 const onPressLanguageSettings = React.useCallback(() => { 231 226 navigation.navigate('LanguageSettings') ··· 414 409 415 410 <View style={styles.spacer20} /> 416 411 417 - {/* TODO ( 418 - <> 419 - <Text type="xl-bold" style={[pal.text, styles.heading]}> 420 - <Trans>Invite a Friend</Trans> 421 - </Text> 422 - 423 - <TouchableOpacity 424 - testID="inviteFriendBtn" 425 - style={[ 426 - styles.linkCard, 427 - pal.view, 428 - isSwitchingAccounts && styles.dimmed, 429 - ]} 430 - onPress={isSwitchingAccounts ? undefined : onPressInviteCodes} 431 - accessibilityRole="button" 432 - accessibilityLabel={_(msg`Invite`)} 433 - accessibilityHint={_(msg`Opens invite code list`)} 434 - disabled={invites?.disabled}> 435 - <View 436 - style={[ 437 - styles.iconContainer, 438 - invitesAvailable > 0 ? primaryBg : pal.btn, 439 - ]}> 440 - <FontAwesomeIcon 441 - icon="ticket" 442 - style={ 443 - (invitesAvailable > 0 444 - ? primaryText 445 - : pal.text) as FontAwesomeIconStyle 446 - } 447 - /> 448 - </View> 449 - <Text 450 - type="lg" 451 - style={invitesAvailable > 0 ? pal.link : pal.text}> 452 - {invites?.disabled ? ( 453 - <Trans> 454 - Your invite codes are hidden when logged in using an App 455 - Password 456 - </Trans> 457 - ) : invitesAvailable === 1 ? ( 458 - <Trans>{invitesAvailable} invite code available</Trans> 459 - ) : ( 460 - <Trans>{invitesAvailable} invite codes available</Trans> 461 - )} 462 - </Text> 463 - </TouchableOpacity> 464 - 465 - <View style={styles.spacer20} /> 466 - </> 467 - )*/} 468 - 469 412 <Text type="xl-bold" style={[pal.text, styles.heading]}> 470 413 <Trans>Accessibility</Trans> 471 414 </Text> ··· 735 678 labelType="lg" 736 679 isSelected={inAppBrowserPref ?? false} 737 680 onPress={() => setUseInAppBrowser(!inAppBrowserPref)} 681 + /> 682 + </View> 683 + )} 684 + {isNative && ( 685 + <View style={[pal.view, styles.toggleCard]}> 686 + <ToggleButton 687 + type="default-light" 688 + label={ 689 + isIOS ? _(msg`Disable haptics`) : _(msg`Disable vibrations`) 690 + } 691 + labelType="lg" 692 + isSelected={isHapticsDisabled} 693 + onPress={() => setHapticsDisabled(!isHapticsDisabled)} 738 694 /> 739 695 </View> 740 696 )}
+4 -3
src/view/shell/bottom-bar/BottomBar.tsx
··· 8 8 import {StackActions} from '@react-navigation/native' 9 9 10 10 import {useAnalytics} from '#/lib/analytics/analytics' 11 - import {Haptics} from '#/lib/haptics' 11 + import {useHaptics} from '#/lib/haptics' 12 12 import {useDedupe} from '#/lib/hooks/useDedupe' 13 13 import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode' 14 14 import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState' ··· 59 59 const closeAllActiveElements = useCloseAllActiveElements() 60 60 const dedupe = useDedupe() 61 61 const accountSwitchControl = useDialogControl() 62 + const playHaptic = useHaptics() 62 63 63 64 const showSignIn = React.useCallback(() => { 64 65 closeAllActiveElements() ··· 104 105 }, [onPressTab]) 105 106 106 107 const onLongPressProfile = React.useCallback(() => { 107 - Haptics.default() 108 + playHaptic() 108 109 accountSwitchControl.open() 109 - }, [accountSwitchControl]) 110 + }, [accountSwitchControl, playHaptic]) 110 111 111 112 return ( 112 113 <>