Skip to content

Commit

Permalink
improve android haptics, offer toggle for haptics
Browse files Browse the repository at this point in the history
  • Loading branch information
haileyok committed Apr 11, 2024
1 parent 9007810 commit 7b413d0
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 168 deletions.
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)
20 changes: 12 additions & 8 deletions src/lib/haptics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/screens/Profile/Header/ProfileHeaderLabeler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

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

if (likeUri) {
await unlikeMod({uri: likeUri})
Expand All @@ -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')
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: true,
}
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
23 changes: 16 additions & 7 deletions src/view/com/util/post-ctrls/PostCtrls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -67,6 +68,7 @@ let PostCtrls = ({
)
const requireAuth = useRequireAuth()
const loggedOutWarningPromptControl = useDialogControl()
const isHapticsDisabled = useHapticsDisabled()

const shouldShowLoggedOutWarning = React.useMemo(() => {
return !!post.author.labels?.find(
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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(() => {
Expand Down
39 changes: 31 additions & 8 deletions src/view/screens/ProfileFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -159,6 +160,7 @@ export function ProfileFeedScreenInner({
const reportDialogControl = useReportDialogControl()
const {openComposer} = useComposerControls()
const {track} = useAnalytics()
const isHapticsDisabled = useHapticsDisabled()
const feedSectionRef = React.useRef<SectionRef>(null)
const isScreenFocused = useIsFocused()

Expand Down Expand Up @@ -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})
Expand All @@ -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})
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -527,7 +540,7 @@ function AboutSection({

const onToggleLiked = React.useCallback(async () => {
try {
Haptics.default()
Haptics.default(isHapticsDisabled)

if (isLiked && likeUri) {
await unlikeFeed({uri: likeUri})
Expand All @@ -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 (
<View style={[styles.aboutSectionContainer]}>
Expand Down
Loading

0 comments on commit 7b413d0

Please sign in to comment.