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 themzio/stable
parent
9007810cdb
commit
740cd029d7
|
@ -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.
|
|
@ -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)
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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>
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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]}>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
|
|
Loading…
Reference in New Issue