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
zio/stable
Hailey 2024-04-11 15:20:26 -07:00 committed by GitHub
parent 9007810cdb
commit 740cd029d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 235 additions and 204 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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])
}

View File

@ -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')

View File

@ -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<LegacySchema>): Schema {
externalEmbeds: defaults.externalEmbeds,
lastSelectedHomeFeed: defaults.lastSelectedHomeFeed,
pdsAddressHistory: defaults.pdsAddressHistory,
disableHaptics: defaults.disableHaptics,
}
}

View File

@ -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<typeof schema>
@ -93,4 +95,5 @@ export const defaults: Schema = {
useInAppBrowser: undefined,
lastSelectedHomeFeed: undefined,
pdsAddressHistory: [],
disableHaptics: false,
}

View File

@ -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)

View File

@ -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<{}>) {
<AltTextRequiredProvider>
<ExternalEmbedsProvider>
<HiddenPostsProvider>
<InAppBrowserProvider>{children}</InAppBrowserProvider>
<InAppBrowserProvider>
<DisableHapticsProvider>{children}</DisableHapticsProvider>
</InAppBrowserProvider>
</HiddenPostsProvider>
</ExternalEmbedsProvider>
</AltTextRequiredProvider>

View File

@ -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(() => {

View File

@ -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<SectionRef>(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 (
<View style={[styles.aboutSectionContainer]}>

View File

@ -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 {

View File

@ -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

View File

@ -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<NavigationProp>()
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) {
<View style={styles.spacer20} />
{/* TODO (
<>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Invite a Friend</Trans>
</Text>
<TouchableOpacity
testID="inviteFriendBtn"
style={[
styles.linkCard,
pal.view,
isSwitchingAccounts && styles.dimmed,
]}
onPress={isSwitchingAccounts ? undefined : onPressInviteCodes}
accessibilityRole="button"
accessibilityLabel={_(msg`Invite`)}
accessibilityHint={_(msg`Opens invite code list`)}
disabled={invites?.disabled}>
<View
style={[
styles.iconContainer,
invitesAvailable > 0 ? primaryBg : pal.btn,
]}>
<FontAwesomeIcon
icon="ticket"
style={
(invitesAvailable > 0
? primaryText
: pal.text) as FontAwesomeIconStyle
}
/>
</View>
<Text
type="lg"
style={invitesAvailable > 0 ? pal.link : pal.text}>
{invites?.disabled ? (
<Trans>
Your invite codes are hidden when logged in using an App
Password
</Trans>
) : invitesAvailable === 1 ? (
<Trans>{invitesAvailable} invite code available</Trans>
) : (
<Trans>{invitesAvailable} invite codes available</Trans>
)}
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
</>
)*/}
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Accessibility</Trans>
</Text>
@ -738,6 +681,19 @@ export function SettingsScreen({}: Props) {
/>
</View>
)}
{isNative && (
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
type="default-light"
label={
isIOS ? _(msg`Disable haptics`) : _(msg`Disable vibrations`)
}
labelType="lg"
isSelected={isHapticsDisabled}
onPress={() => setHapticsDisabled(!isHapticsDisabled)}
/>
</View>
)}
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
<Trans>Account</Trans>

View File

@ -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 (
<>