Move /platform/detection vars into /env (#9707)

* Add platform vars to env

* Replace /platform/detection with /env
This commit is contained in:
Eric Bailey
2026-01-16 16:28:17 -06:00
committed by GitHub
parent bd510d8468
commit 0589bd7d9e
244 changed files with 789 additions and 789 deletions
+2 -2
View File
@@ -1,11 +1,11 @@
import {AtUri} from '@atproto/api'
import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
import {isWeb} from '#/platform/detection'
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
import {IS_WEB} from '#/env'
let debugTopics = ''
if (isWeb && typeof window !== 'undefined') {
if (IS_WEB && typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search)
debugTopics = params.get('debug_topics') ?? ''
}
+1 -1
View File
@@ -1,5 +1,5 @@
export const isSafari = false
export const isFirefox = false
export const isTouchDevice = true
export const isAndroidWeb = false
export const IS_ANDROIDWeb = false
export const isHighDPI = true
+1 -1
View File
@@ -4,6 +4,6 @@ export const isSafari = /^((?!chrome|android).)*safari/i.test(
)
export const isFirefox = /firefox|fxios/i.test(navigator.userAgent)
export const isTouchDevice = window.matchMedia('(pointer: coarse)').matches
export const isAndroidWeb =
export const IS_ANDROIDWeb =
/android/i.test(navigator.userAgent) && isTouchDevice
export const isHighDPI = window.matchMedia('(min-resolution: 2dppx)').matches
@@ -13,7 +13,7 @@ import Animated, {
withTiming,
} from 'react-native-reanimated'
import {isIOS, isWeb} from '#/platform/detection'
import {IS_IOS, IS_WEB} from '#/env'
type AccordionAnimationProps = React.PropsWithChildren<{
isExpanded: boolean
@@ -66,12 +66,12 @@ function MobileAccordion({
style={style}
entering={FadeInUp.duration(duration)}
exiting={FadeOutUp.duration(duration / 2)}
pointerEvents={isIOS ? 'auto' : 'box-none'}>
pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
{children}
</Animated.View>
)
}
export function AccordionAnimation(props: AccordionAnimationProps) {
return isWeb ? <WebAccordion {...props} /> : <MobileAccordion {...props} />
return IS_WEB ? <WebAccordion {...props} /> : <MobileAccordion {...props} />
}
+2 -2
View File
@@ -13,9 +13,9 @@ import Animated, {
} from 'react-native-reanimated'
import {isTouchDevice} from '#/lib/browser'
import {isNative} from '#/platform/detection'
import {IS_NATIVE} from '#/env'
const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1
const DEFAULT_TARGET_SCALE = IS_NATIVE || isTouchDevice ? 0.98 : 1
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
+3 -3
View File
@@ -2,20 +2,20 @@ import React from 'react'
import * as Device from 'expo-device'
import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
import {isIOS, isWeb} from '#/platform/detection'
import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
import {IS_IOS, IS_WEB} from '#/env'
export function useHaptics() {
const isHapticsDisabled = useHapticsDisabled()
return React.useCallback(
(strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
if (isHapticsDisabled || isWeb) {
if (isHapticsDisabled || IS_WEB) {
return
}
// Users said the medium impact was too strong on Android; see APP-537s
const style = isIOS
const style = IS_IOS
? ImpactFeedbackStyle[strength]
: ImpactFeedbackStyle.Light
impactAsync(style)
+2 -2
View File
@@ -3,10 +3,10 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {type SessionAccount, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {IS_WEB} from '#/env'
import {logEvent} from '../statsig/statsig'
import {type LogEvents} from '../statsig/statsig'
@@ -28,7 +28,7 @@ export function useAccountSwitcher() {
try {
setPendingDid(account.did)
if (account.accessJwt) {
if (isWeb) {
if (IS_WEB) {
// We're switching accounts, which remounts the entire app.
// On mobile, this gets us Home, but on the web we also need reset the URL.
// We can't change the URL via a navigate() call because the navigator
+2 -2
View File
@@ -2,13 +2,13 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {clamp} from '#/lib/numbers'
import {isWeb} from '#/platform/detection'
import {IS_WEB} from '#/env'
export function useBottomBarOffset(modifier: number = 0) {
const {isTabletOrDesktop} = useWebMediaQueries()
const {bottom: bottomInset} = useSafeAreaInsets()
return (
(isWeb && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) +
(IS_WEB && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) +
modifier
)
}
+3 -3
View File
@@ -6,10 +6,10 @@ import * as WebBrowser from 'expo-web-browser'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
import {logger} from '#/logger'
import {isIOS, isNative} from '#/platform/detection'
import {useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {useIntentDialogs} from '#/components/intents/IntentDialogs'
import {IS_IOS, IS_NATIVE} from '#/env'
import {Referrer} from '../../../modules/expo-bluesky-swiss-army'
import {useApplyPullRequestOTAUpdate} from './useOTAUpdates'
@@ -29,7 +29,7 @@ export function useIntentHandler() {
React.useEffect(() => {
const handleIncomingURL = async (url: string) => {
if (isIOS) {
if (IS_IOS) {
// Close in-app browser if it's open (iOS only)
await WebBrowser.dismissBrowser().catch(() => {})
}
@@ -150,7 +150,7 @@ export function useComposeIntent() {
setTimeout(() => {
openComposer({
text: text ?? undefined,
imageUris: isNative ? imageUris : undefined,
imageUris: IS_NATIVE ? imageUris : undefined,
})
}, 500)
},
+3 -3
View File
@@ -1,7 +1,7 @@
import {useEffect, useState} from 'react'
import {Keyboard} from 'react-native'
import {isIOS} from '#/platform/detection'
import {IS_IOS} from '#/env'
export function useIsKeyboardVisible({
iosUseWillEvents,
@@ -14,9 +14,9 @@ export function useIsKeyboardVisible({
// only iOS supports the "will" events
// -prf
const showEvent =
isIOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow'
IS_IOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow'
const hideEvent =
isIOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide'
IS_IOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide'
useEffect(() => {
const keyboardShowListener = Keyboard.addListener(showEvent, () =>
+3 -3
View File
@@ -9,7 +9,6 @@ import {useQueryClient} from '@tanstack/react-query'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {logger as notyLogger} from '#/lib/notifications/util'
import {type NavigationProp} from '#/lib/routes/types'
import {isAndroid, isIOS} from '#/platform/detection'
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
@@ -17,6 +16,7 @@ import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {IS_ANDROID, IS_IOS} from '#/env'
import {resetToTab} from '#/Navigation'
import {router} from '#/routes'
@@ -90,7 +90,7 @@ export function useNotificationsHandler() {
// channels allow for the mute/unmute functionality we want for the background
// handler.
useEffect(() => {
if (!isAndroid) return
if (!IS_ANDROID) return
// assign both chat notifications to a group
// NOTE: I don't think that it will retroactively move them into the group
// if the channels already exist. no big deal imo -sfn
@@ -379,7 +379,7 @@ export function getNotificationPayload(
}
const payload = (
isIOS ? e.request.trigger.payload : e.request.content.data
IS_IOS ? e.request.trigger.payload : e.request.content.data
) as NotificationPayload
if (payload && payload.reason) {
+4 -4
View File
@@ -12,14 +12,14 @@ import {
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
import {IS_ANDROID, IS_IOS} from '#/env'
import {IS_TESTFLIGHT} from '#/env'
const MINIMUM_MINIMIZE_TIME = 15 * 60e3
async function setExtraParams() {
await setExtraParamAsync(
isIOS ? 'ios-build-number' : 'android-build-number',
IS_IOS ? 'ios-build-number' : 'android-build-number',
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
// This just ensures it gets passed as a string
`${nativeBuildVersion}`,
@@ -32,7 +32,7 @@ async function setExtraParams() {
async function setExtraParamsPullRequest(channel: string) {
await setExtraParamAsync(
isIOS ? 'ios-build-number' : 'android-build-number',
IS_IOS ? 'ios-build-number' : 'android-build-number',
// Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
// This just ensures it gets passed as a string
`${nativeBuildVersion}`,
@@ -198,7 +198,7 @@ export function useOTAUpdates() {
// `maintainVisibleContentPosition`. See repro repo for more details:
// https://github.com/mozzius/ota-crash-repro
// Old Arch only - re-enable once we're on the New Archictecture! -sfn
if (isAndroid) return
if (IS_ANDROID) return
const subscription = AppState.addEventListener(
'change',
+2 -2
View File
@@ -12,11 +12,11 @@ import {
toNiceDomain,
} from '#/lib/strings/url-helpers'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useInAppBrowser} from '#/state/preferences/in-app-browser'
import {useTheme} from '#/alf'
import {useDialogContext} from '#/components/Dialog'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {IS_NATIVE} from '#/env'
export function useOpenLink() {
const enabled = useInAppBrowser()
@@ -41,7 +41,7 @@ export function useOpenLink() {
}
}
if (isNative && !url.startsWith('mailto:')) {
if (IS_NATIVE && !url.startsWith('mailto:')) {
if (override === undefined && enabled === undefined) {
// consent dialog is a global dialog, and while it's possible to nest dialogs,
// the actual components need to be nested. sibling dialogs on iOS are not supported.
+3 -3
View File
@@ -2,8 +2,8 @@ import {Linking} from 'react-native'
import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
import * as MediaLibrary from 'expo-media-library'
import {isWeb} from '#/platform/detection'
import {Alert} from '#/view/com/util/Alert'
import {IS_WEB} from '#/env'
const openPermissionAlert = (perm: string) => {
Alert.alert(
@@ -26,7 +26,7 @@ export function usePhotoLibraryPermission() {
const requestPhotoAccessIfNeeded = async () => {
// On the, we use <input type="file"> to produce a filepicker
// This does not need any permission granting.
if (isWeb) {
if (IS_WEB) {
return true
}
@@ -55,7 +55,7 @@ export function useVideoLibraryPermission() {
const requestVideoAccessIfNeeded = async () => {
// On the, we use <input type="file"> to produce a filepicker
// This does not need any permission granting.
if (isWeb) {
if (IS_WEB) {
return true
}
+2 -2
View File
@@ -2,7 +2,7 @@ import {useCallback} from 'react'
import * as IntentLauncher from 'expo-intent-launcher'
import {getTranslatorLink} from '#/locale/helpers'
import {isAndroid} from '#/platform/detection'
import {IS_ANDROID} from '#/env'
import {useOpenLink} from './useOpenLink'
export function useTranslate() {
@@ -11,7 +11,7 @@ export function useTranslate() {
return useCallback(
async (text: string, language: string) => {
const translateUrl = getTranslatorLink(text, language)
if (isAndroid) {
if (IS_ANDROID) {
try {
// use getApplicationIconAsync to determine if the translate app is installed
if (
+2 -2
View File
@@ -1,6 +1,6 @@
import {useMediaQuery} from 'react-responsive'
import {isNative} from '#/platform/detection'
import {IS_NATIVE} from '#/env'
/**
* @deprecated use `useBreakpoints` from `#/alf` instead
@@ -11,7 +11,7 @@ export function useWebMediaQueries() {
const isMobile = useMediaQuery({maxWidth: 800 - 1})
const isTabletOrMobile = isMobile || isTablet
const isTabletOrDesktop = isDesktop || isTablet
if (isNative) {
if (IS_NATIVE) {
return {
isMobile: true,
isTablet: false,
+4 -4
View File
@@ -18,7 +18,7 @@ import {Buffer} from 'buffer'
import {POST_IMG_MAX} from '#/lib/constants'
import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
import {IS_ANDROID, IS_IOS} from '#/env'
import {type PickerImage} from './picker.shared'
import {type Dimensions} from './types'
@@ -108,7 +108,7 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
// save
try {
if (isAndroid) {
if (IS_ANDROID) {
// android triggers an annoying permission prompt if you try and move an image
// between albums. therefore, we need to either create the album with the image
// as the starting image, or put it directly into the album
@@ -305,7 +305,7 @@ function joinPath(a: string, b: string) {
}
function normalizePath(str: string, allPlatforms = false): string {
if (isAndroid || allPlatforms) {
if (IS_ANDROID || allPlatforms) {
if (!str.startsWith('file://')) {
return `file://${str}`
}
@@ -328,7 +328,7 @@ export async function saveToDevice(
type: string,
) {
try {
if (isIOS) {
if (IS_IOS) {
await withTempFile(filename, encoded, async tmpFileUrl => {
await Sharing.shareAsync(tmpFileUrl, {UTI: type})
})
+3 -3
View File
@@ -5,9 +5,9 @@ import {
} from 'expo-image-picker'
import {t} from '@lingui/macro'
import {isIOS, isWeb} from '#/platform/detection'
import {type ImageMeta} from '#/state/gallery'
import * as Toast from '#/view/com/util/Toast'
import {IS_IOS, IS_WEB} from '#/env'
import {VIDEO_MAX_DURATION_MS} from '../constants'
import {getDataUriSize} from './util'
@@ -53,8 +53,8 @@ export async function openUnifiedPicker({
quality: 1,
allowsMultipleSelection: true,
legacy: true,
base64: isWeb,
selectionLimit: isIOS ? selectionCountRemaining : undefined,
base64: IS_WEB,
selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
preferredAssetRepresentationMode:
UIImagePickerPreferredAssetRepresentationMode.Automatic,
videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
+2 -2
View File
@@ -2,8 +2,8 @@ import {useCallback} from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import * as Toast from '#/components/Toast'
import {IS_NATIVE} from '#/env'
import {saveImageToMediaLibrary} from './manip'
/**
@@ -16,7 +16,7 @@ export function useSaveImageToMediaLibrary() {
const {_} = useLingui()
return useCallback(
async (uri: string) => {
if (!isNative) {
if (!IS_NATIVE) {
throw new Error('useSaveImageToMediaLibrary is native only')
}
+2 -2
View File
@@ -3,8 +3,8 @@ import * as MediaLibrary from 'expo-media-library'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import * as Toast from '#/components/Toast'
import {IS_NATIVE} from '#/env'
import {saveImageToMediaLibrary} from './manip'
/**
@@ -18,7 +18,7 @@ export function useSaveImageToMediaLibrary() {
})
return useCallback(
async (uri: string) => {
if (!isNative) {
if (!IS_NATIVE) {
throw new Error('useSaveImageToMediaLibrary is native only')
}
+5 -5
View File
@@ -13,10 +13,10 @@ import {
} from '#/lib/constants'
import {logger as notyLogger} from '#/lib/notifications/util'
import {isNetworkError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {type SessionAccount, useAgent, useSession} from '#/state/session'
import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
import {useAgeAssurance} from '#/ageAssurance'
import {IS_NATIVE} from '#/env'
import {IS_DEV} from '#/env'
/**
@@ -140,7 +140,7 @@ export function useGetAndRegisterPushToken() {
}: {
isAgeRestricted?: boolean
} = {}) => {
if (!isNative || IS_DEV) return
if (!IS_NATIVE || IS_DEV) return
/**
* This will also fire the listener added via `addPushTokenListener`. That
@@ -236,7 +236,7 @@ export function useRequestNotificationsPermission() {
const permissions = await Notifications.getPermissionsAsync()
if (
!isNative ||
!IS_NATIVE ||
permissions?.status === 'granted' ||
(permissions?.status === 'denied' && !permissions.canAskAgain)
) {
@@ -277,7 +277,7 @@ export function useRequestNotificationsPermission() {
}
export async function decrementBadgeCount(by: number) {
if (!isNative) return
if (!IS_NATIVE) return
let count = await getBadgeCountAsync()
count -= by
@@ -295,7 +295,7 @@ export async function resetBadgeCount() {
}
export async function unregisterPushToken(agents: AtpAgent[]) {
if (!isNative) return
if (!IS_NATIVE) return
try {
const token = await getPushToken()
+3 -3
View File
@@ -9,8 +9,8 @@ import {
} from '@tanstack/react-query-persist-client'
import type React from 'react'
import {isNative, isWeb} from '#/platform/detection'
import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
import {IS_NATIVE, IS_WEB} from '#/env'
declare global {
interface Window {
@@ -87,7 +87,7 @@ setInterval(() => {
}, 2000)
focusManager.setEventListener(onFocus => {
if (isNative) {
if (IS_NATIVE) {
const subscription = AppState.addEventListener(
'change',
(status: AppStateStatus) => {
@@ -187,7 +187,7 @@ function QueryProviderInner({
}
})
useEffect(() => {
if (isWeb) {
if (IS_WEB) {
window.__TANSTACK_QUERY_CLIENT__ = queryClient
}
}, [queryClient])
+4 -4
View File
@@ -4,8 +4,8 @@ import {setStringAsync} from 'expo-clipboard'
// TODO: replace global i18n instance with one returned from useLingui -sfn
import {t} from '@lingui/macro'
import {isAndroid, isIOS} from '#/platform/detection'
import * as Toast from '#/view/com/util/Toast'
import {IS_ANDROID, IS_IOS} from '#/env'
/**
* This function shares a URL using the native Share API if available, or copies it to the clipboard
@@ -14,9 +14,9 @@ import * as Toast from '#/view/com/util/Toast'
* clipboard.
*/
export async function shareUrl(url: string) {
if (isAndroid) {
if (IS_ANDROID) {
await Share.share({message: url})
} else if (isIOS) {
} else if (IS_IOS) {
await Share.share({url})
} else {
// React Native Share is not supported by web. Web Share API
@@ -34,7 +34,7 @@ export async function shareUrl(url: string) {
* clipboard.
*/
export async function shareText(text: string) {
if (isAndroid || isIOS) {
if (IS_ANDROID || IS_IOS) {
await Share.share({message: text})
} else {
await setStringAsync(text)
+2 -2
View File
@@ -5,8 +5,8 @@ import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics'
import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted'
import {IS_WEB} from '#/env'
import * as env from '#/env'
import {useSession} from '../../state/session'
import {timeout} from '../async/timeout'
@@ -37,7 +37,7 @@ type StatsigUser = {
let refSrc = ''
let refUrl = ''
if (isWeb && typeof window !== 'undefined') {
if (IS_WEB && typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search)
refSrc = params.get('ref_src') ?? ''
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
+4 -4
View File
@@ -1,11 +1,11 @@
import {Dimensions} from 'react-native'
import {isSafari} from '#/lib/browser'
import {isWeb} from '#/platform/detection'
import {IS_WEB} from '#/env'
const {height: SCREEN_HEIGHT} = Dimensions.get('window')
const IFRAME_HOST = isWeb
const IFRAME_HOST = IS_WEB
? // @ts-ignore only for web
window.location.host === 'localhost:8100'
? 'http://localhost:8100'
@@ -132,7 +132,7 @@ export function parseEmbedPlayerFromUrl(
urlp.hostname === 'www.twitch.tv' ||
urlp.hostname === 'm.twitch.tv'
) {
const parent = isWeb
const parent = IS_WEB
? // @ts-ignore only for web
window.location.hostname
: 'localhost'
@@ -559,7 +559,7 @@ export function parseTenorGif(urlp: URL):
width: Number(w),
}
if (isWeb) {
if (IS_WEB) {
if (isSafari) {
id = id.replace('AAAAC', 'AAAP1')
filename = filename.replace('.gif', '.mp4')
+2 -2
View File
@@ -5,7 +5,7 @@ import {
type TextStyle,
} from 'react-native'
import {isWeb} from '#/platform/detection'
import {IS_WEB} from '#/env'
import {type Theme, type TypographyVariant} from './ThemeContext'
// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
@@ -186,7 +186,7 @@ export const s = StyleSheet.create({
// dimensions
w100pct: {width: '100%'},
h100pct: {height: '100%'},
hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'},
hContentRegion: IS_WEB ? {minHeight: '100%'} : {height: '100%'},
window: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,