Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Android haptic, offer toggle for haptics in the app #3482

Merged
merged 7 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions patches/expo-haptics+12.8.1.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions patches/expo-haptics+12.8.1.patch
Original file line number Diff line number Diff line change
@@ -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)
45 changes: 9 additions & 36 deletions src/lib/haptics.ts
Original file line number Diff line number Diff line change
@@ -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 {
haileyok marked this conversation as resolved.
Show resolved Hide resolved
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])
}
7 changes: 4 additions & 3 deletions src/screens/Profile/Header/ProfileHeaderLabeler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -93,7 +94,7 @@ let ProfileHeaderLabeler = ({
return
}
try {
Haptics.default()
playHaptic()

if (likeUri) {
await unlikeMod({uri: likeUri})
Expand All @@ -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')
Expand Down
3 changes: 2 additions & 1 deletion src/state/persisted/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -113,6 +113,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
externalEmbeds: defaults.externalEmbeds,
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
pdsAddressHistory: defaults.pdsAddressHistory,
disableHaptics: defaults.disableHaptics,
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/state/persisted/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {z} from 'zod'

import {deviceLocales} from '#/platform/detection'

const externalEmbedOptions = ['show', 'hide'] as const
Expand Down Expand Up @@ -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<typeof schema>

Expand Down Expand Up @@ -93,4 +95,5 @@ export const defaults: Schema = {
useInAppBrowser: undefined,
lastSelectedHomeFeed: undefined,
pdsAddressHistory: [],
disableHaptics: false,
}
42 changes: 42 additions & 0 deletions src/state/preferences/disable-haptics.tsx
Original file line number Diff line number Diff line change
@@ -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<StateContext>(
Boolean(persisted.defaults.disableHaptics),
)
const setContext = React.createContext<SetContext>((_: 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 (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}

export const useHapticsDisabled = () => React.useContext(stateContext)
export const useSetHapticsDisabled = () => React.useContext(setContext)
10 changes: 7 additions & 3 deletions src/state/preferences/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -16,14 +17,17 @@ 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 (
<LanguagesProvider>
<AltTextRequiredProvider>
<ExternalEmbedsProvider>
<HiddenPostsProvider>
<InAppBrowserProvider>{children}</InAppBrowserProvider>
<InAppBrowserProvider>
<DisableHapticsProvider>{children}</DisableHapticsProvider>
</InAppBrowserProvider>
</HiddenPostsProvider>
</ExternalEmbedsProvider>
</AltTextRequiredProvider>
Expand Down
18 changes: 10 additions & 8 deletions src/view/com/util/post-ctrls/PostCtrls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -67,6 +67,7 @@ let PostCtrls = ({
)
const requireAuth = useRequireAuth()
const loggedOutWarningPromptControl = useDialogControl()
const playHaptic = useHaptics()

const shouldShowLoggedOutWarning = React.useMemo(() => {
return !!post.author.labels?.find(
Expand All @@ -84,7 +85,7 @@ let PostCtrls = ({
const onPressToggleLike = React.useCallback(async () => {
try {
if (!post.viewer?.like) {
Haptics.default()
playHaptic()
await queueLike()
} else {
await queueUnlike()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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(() => {
Expand Down
Loading
Loading