···11import {useSafeAreaInsets} from 'react-native-safe-area-context'
12import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
1314-import {isIOS} from '#/platform/detection'
15import {
16 type BottomSheetState,
17 type BottomSheetViewProps,
···3031const NativeModule = requireNativeModule('BottomSheet')
3233-const isIOS15 =
34 Platform.OS === 'ios' &&
35 // semvar - can be 3 segments, so can't use Number(Platform.Version)
36 Number(Platform.Version.split('.').at(0)) < 16
···91 }
9293 let extraStyles
94- if (isIOS15 && this.state.viewHeight) {
95 const {viewHeight} = this.state
96 const cornerRadius = this.props.cornerRadius ?? 0
97 if (viewHeight < screenHeight / 2) {
···112 onStateChange={this.onStateChange}
113 extraStyles={extraStyles}
114 onLayout={e => {
115- if (isIOS15) {
116 const {height} = e.nativeEvent.layout
117 this.setState({viewHeight: height})
118 }
···153 const insets = useSafeAreaInsets()
154 const cornerRadius = rest.cornerRadius ?? 0
155156- const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight
157158 return (
159 <NativeView
···11import {useSafeAreaInsets} from 'react-native-safe-area-context'
12import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
1314+import {IS_IOS} from '#/env'
15import {
16 type BottomSheetState,
17 type BottomSheetViewProps,
···3031const NativeModule = requireNativeModule('BottomSheet')
3233+const IS_IOS15 =
34 Platform.OS === 'ios' &&
35 // semvar - can be 3 segments, so can't use Number(Platform.Version)
36 Number(Platform.Version.split('.').at(0)) < 16
···91 }
9293 let extraStyles
94+ if (IS_IOS15 && this.state.viewHeight) {
95 const {viewHeight} = this.state
96 const cornerRadius = this.props.cornerRadius ?? 0
97 if (viewHeight < screenHeight / 2) {
···112 onStateChange={this.onStateChange}
113 extraStyles={extraStyles}
114 onLayout={e => {
115+ if (IS_IOS15) {
116 const {height} = e.nativeEvent.layout
117 this.setState({viewHeight: height})
118 }
···153 const insets = useSafeAreaInsets()
154 const cornerRadius = rest.cornerRadius ?? 0
155156+ const sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight
157158 return (
159 <NativeView
+3-3
src/App.native.tsx
···23import {ThemeProvider} from '#/lib/ThemeContext'
24import I18nProvider from '#/locale/i18nProvider'
25import {logger} from '#/logger'
26-import {isAndroid, isIOS} from '#/platform/detection'
27import {Provider as A11yProvider} from '#/state/a11y'
28import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
29import {Provider as DialogStateProvider} from '#/state/dialogs'
···69import {ToastOutlet} from '#/components/Toast'
70import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
71import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
072import {
73 prefetchLiveEvents,
74 Provider as LiveEventsProvider,
···79import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
8081SplashScreen.preventAutoHideAsync()
82-if (isIOS) {
83 SystemUI.setBackgroundColorAsync('black')
84}
85-if (isAndroid) {
86 // iOS is handled by the config plugin -sfn
87 ScreenOrientation.lockAsync(
88 ScreenOrientation.OrientationLock.PORTRAIT_UP,
···23import {ThemeProvider} from '#/lib/ThemeContext'
24import I18nProvider from '#/locale/i18nProvider'
25import {logger} from '#/logger'
026import {Provider as A11yProvider} from '#/state/a11y'
27import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
28import {Provider as DialogStateProvider} from '#/state/dialogs'
···68import {ToastOutlet} from '#/components/Toast'
69import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
70import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
71+import {IS_ANDROID, IS_IOS} from '#/env'
72import {
73 prefetchLiveEvents,
74 Provider as LiveEventsProvider,
···79import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
8081SplashScreen.preventAutoHideAsync()
82+if (IS_IOS) {
83 SystemUI.setBackgroundColorAsync('black')
84}
85+if (IS_ANDROID) {
86 // iOS is handled by the config plugin -sfn
87 ScreenOrientation.lockAsync(
88 ScreenOrientation.OrientationLock.PORTRAIT_UP,
+6-6
src/Navigation.tsx
···44import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
45import {bskyTitle} from '#/lib/strings/headings'
46import {logger} from '#/logger'
47-import {isNative, isWeb} from '#/platform/detection'
48import {useUnreadNotifications} from '#/state/queries/notifications/unread'
49import {useSession} from '#/state/session'
50import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···138 EmailDialogScreenID,
139 useEmailDialogControl,
140} from '#/components/dialogs/EmailDialog'
0141import {router} from '#/routes'
142import {Referrer} from '../modules/expo-bluesky-swiss-army'
143···842 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
843 // since it will be created by react-navigation.
844 if (path.includes('intent/')) {
845- if (isNative) return
846 return buildStateObject('Flat', 'Home', params)
847 }
848849- if (isNative) {
850 if (name === 'Search') {
851 return buildStateObject('SearchTab', 'Search', params)
852 }
···921 )
922923 async function handlePushNotificationEntry() {
924- if (!isNative) return
925926 // deep links take precedence - on android,
927 // getLastNotificationResponseAsync returns a "notification"
···1069 navigationRef.dispatch(
1070 CommonActions.reset({
1071 index: 0,
1072- routes: [{name: isNative ? 'HomeTab' : 'Home'}],
1073 }),
1074 )
1075 return Promise.race([
···1103 initMs,
1104 })
11051106- if (isWeb) {
1107 const referrerInfo = Referrer.getReferrerInfo()
1108 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
1109 logEvent('deepLink:referrerReceived', {
···44import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
45import {bskyTitle} from '#/lib/strings/headings'
46import {logger} from '#/logger'
047import {useUnreadNotifications} from '#/state/queries/notifications/unread'
48import {useSession} from '#/state/session'
49import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···137 EmailDialogScreenID,
138 useEmailDialogControl,
139} from '#/components/dialogs/EmailDialog'
140+import {IS_NATIVE, IS_WEB} from '#/env'
141import {router} from '#/routes'
142import {Referrer} from '../modules/expo-bluesky-swiss-army'
143···842 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
843 // since it will be created by react-navigation.
844 if (path.includes('intent/')) {
845+ if (IS_NATIVE) return
846 return buildStateObject('Flat', 'Home', params)
847 }
848849+ if (IS_NATIVE) {
850 if (name === 'Search') {
851 return buildStateObject('SearchTab', 'Search', params)
852 }
···921 )
922923 async function handlePushNotificationEntry() {
924+ if (!IS_NATIVE) return
925926 // deep links take precedence - on android,
927 // getLastNotificationResponseAsync returns a "notification"
···1069 navigationRef.dispatch(
1070 CommonActions.reset({
1071 index: 0,
1072+ routes: [{name: IS_NATIVE ? 'HomeTab' : 'Home'}],
1073 }),
1074 )
1075 return Promise.race([
···1103 initMs,
1104 })
11051106+ if (IS_WEB) {
1107 const referrerInfo = Referrer.getReferrerInfo()
1108 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
1109 logEvent('deepLink:referrerReceived', {
+5-5
src/ageAssurance/components/NoAccessScreen.tsx
···10} from '#/lib/hooks/useCreateSupportLink'
11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
12import {logger} from '#/logger'
13-import {isWeb} from '#/platform/detection'
14-import {isNative} from '#/platform/detection'
15import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
16import {useSessionApi} from '#/state/session'
17import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
···38 isLegacyBirthdateBug,
39 useAgeAssuranceRegionConfig,
40} from '#/ageAssurance/util'
0041import {useDeviceGeolocationApi} from '#/geolocation'
4243const textStyles = [a.text_md, a.leading_snug]
···74 }, [])
7576 const onPressLogout = useCallback(() => {
77- if (isWeb) {
78 // We're switching accounts, which remounts the entire app.
79 // On mobile, this gets us Home, but on the web we also need reset the URL.
80 // We can't change the URL via a navigate() call because the navigator
···139 contentContainerStyle={[
140 a.px_2xl,
141 {
142- paddingTop: isWeb
143 ? a.p_5xl.padding
144 : insets.top + a.p_2xl.padding,
145 paddingBottom: 100,
···359 )}
360361 <View style={[a.gap_xs]}>
362- {isNative && (
363 <>
364 <Admonition>
365 <Trans>
···10} from '#/lib/hooks/useCreateSupportLink'
11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
12import {logger} from '#/logger'
0013import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
14import {useSessionApi} from '#/state/session'
15import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
···36 isLegacyBirthdateBug,
37 useAgeAssuranceRegionConfig,
38} from '#/ageAssurance/util'
39+import {IS_WEB} from '#/env'
40+import {IS_NATIVE} from '#/env'
41import {useDeviceGeolocationApi} from '#/geolocation'
4243const textStyles = [a.text_md, a.leading_snug]
···74 }, [])
7576 const onPressLogout = useCallback(() => {
77+ if (IS_WEB) {
78 // We're switching accounts, which remounts the entire app.
79 // On mobile, this gets us Home, but on the web we also need reset the URL.
80 // We can't change the URL via a navigate() call because the navigator
···139 contentContainerStyle={[
140 a.px_2xl,
141 {
142+ paddingTop: IS_WEB
143 ? a.p_5xl.padding
144 : insets.top + a.p_2xl.padding,
145 paddingBottom: 100,
···359 )}
360361 <View style={[a.gap_xs]}>
362+ {IS_NATIVE && (
363 <>
364 <Admonition>
365 <Trans>
+4-4
src/ageAssurance/components/RedirectOverlay.tsx
···15import {retry} from '#/lib/async/retry'
16import {wait} from '#/lib/async/wait'
17import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
18-import {isWeb} from '#/platform/detection'
19-import {isIOS} from '#/platform/detection'
20import {useAgent, useSession} from '#/state/session'
21import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
22import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···28import {Text} from '#/components/Typography'
29import {refetchAgeAssuranceServerState} from '#/ageAssurance'
30import {logger} from '#/ageAssurance'
003132export type RedirectOverlayState = {
33 result: 'success' | 'unknown'
···92 actorDid: params.get('actorDid') ?? undefined,
93 })
9495- if (isWeb) {
96 // Clear the URL parameters so they don't re-trigger
97 history.pushState(null, '', '/')
98 }
···145 // setting a zIndex when using FullWindowOverlay on iOS
146 // means the taps pass straight through to the underlying content (???)
147 // so don't set it on iOS. FullWindowOverlay already does the job.
148- !isIOS && {zIndex: 9999},
149 t.atoms.bg,
150 gtMobile ? a.p_2xl : a.p_xl,
151 a.align_center,
···15import {retry} from '#/lib/async/retry'
16import {wait} from '#/lib/async/wait'
17import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
0018import {useAgent, useSession} from '#/state/session'
19import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
20import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···26import {Text} from '#/components/Typography'
27import {refetchAgeAssuranceServerState} from '#/ageAssurance'
28import {logger} from '#/ageAssurance'
29+import {IS_WEB} from '#/env'
30+import {IS_IOS} from '#/env'
3132export type RedirectOverlayState = {
33 result: 'success' | 'unknown'
···92 actorDid: params.get('actorDid') ?? undefined,
93 })
9495+ if (IS_WEB) {
96 // Clear the URL parameters so they don't re-trigger
97 history.pushState(null, '', '/')
98 }
···145 // setting a zIndex when using FullWindowOverlay on iOS
146 // means the taps pass straight through to the underlying content (???)
147 // so don't set it on iOS. FullWindowOverlay already does the job.
148+ !IS_IOS && {zIndex: 9999},
149 t.atoms.bg,
150 gtMobile ? a.p_2xl : a.p_xl,
151 a.align_center,
+4-4
src/alf/fonts.ts
···1import {type TextStyle} from 'react-native'
23-import {isAndroid, isWeb} from '#/platform/detection'
4import {type Device, device} from '#/storage'
56const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
···39 */
40export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
41 if (fontFamily === 'theme') {
42- if (isAndroid) {
43 style.fontFamily =
44 {
45 400: 'Inter-Regular',
···71 }
72 }
7374- if (isWeb) {
75 // fallback families only supported on web
76 style.fontFamily += `, ${WEB_FONT_FAMILIES}`
77 }
···83 style.fontVariant = (style.fontVariant || []).concat('no-contextual')
84 } else {
85 // fallback families only supported on web
86- if (isWeb) {
87 style.fontFamily = style.fontFamily || WEB_FONT_FAMILIES
88 }
89
···1import {type TextStyle} from 'react-native'
23+import {IS_ANDROID, IS_WEB} from '#/env'
4import {type Device, device} from '#/storage'
56const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
···39 */
40export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
41 if (fontFamily === 'theme') {
42+ if (IS_ANDROID) {
43 style.fontFamily =
44 {
45 400: 'Inter-Regular',
···71 }
72 }
7374+ if (IS_WEB) {
75 // fallback families only supported on web
76 style.fontFamily += `, ${WEB_FONT_FAMILIES}`
77 }
···83 style.fontVariant = (style.fontVariant || []).concat('no-contextual')
84 } else {
85 // fallback families only supported on web
86+ if (IS_WEB) {
87 style.fontFamily = style.fontFamily || WEB_FONT_FAMILIES
88 }
89
+4-4
src/alf/typography.tsx
···4import {UITextView} from 'react-native-uitextview'
5import createEmojiRegex from 'emoji-regex'
67-import {isNative} from '#/platform/detection'
8-import {isIOS} from '#/platform/detection'
9import {type Alf, applyFonts, atoms, flatten} from '#/alf'
001011/**
12 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
···34 if (s.lineHeight !== 0 && s.lineHeight <= 2) {
35 s.lineHeight = Math.round(s.fontSize * s.lineHeight)
36 }
37- } else if (!isNative) {
38 s.lineHeight = s.fontSize
39 }
40···81 props: Omit<TextProps, 'children'> = {},
82 emoji: boolean,
83) {
84- if (!isIOS || !emoji) {
85 return children
86 }
87 return Children.map(children, child => {
···4import {UITextView} from 'react-native-uitextview'
5import createEmojiRegex from 'emoji-regex'
6007import {type Alf, applyFonts, atoms, flatten} from '#/alf'
8+import {IS_NATIVE} from '#/env'
9+import {IS_IOS} from '#/env'
1011/**
12 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
···34 if (s.lineHeight !== 0 && s.lineHeight <= 2) {
35 s.lineHeight = Math.round(s.fontSize * s.lineHeight)
36 }
37+ } else if (!IS_NATIVE) {
38 s.lineHeight = s.fontSize
39 }
40···81 props: Omit<TextProps, 'children'> = {},
82 emoji: boolean,
83) {
84+ if (!IS_IOS || !emoji) {
85 return children
86 }
87 return Children.map(children, child => {
+2-2
src/alf/util/systemUI.ts
···2import {type Theme} from '@bsky.app/alf'
34import {logger} from '#/logger'
5-import {isAndroid} from '#/platform/detection'
67export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
8- if (isAndroid) {
9 try {
10 if (themeType === 'theme') {
11 SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
···2import {type Theme} from '@bsky.app/alf'
34import {logger} from '#/logger'
5+import {IS_ANDROID} from '#/env'
67export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
8+ if (IS_ANDROID) {
9 try {
10 if (themeType === 'theme') {
11 SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
+2-2
src/alf/util/useColorModeTheme.ts
···2import {type ColorSchemeName, useColorScheme} from 'react-native'
3import {type ThemeName} from '@bsky.app/alf'
45-import {isWeb} from '#/platform/detection'
6import {useThemePrefs} from '#/state/shell'
7import {dark, dim, light} from '#/alf/themes'
089export function useColorModeTheme(): ThemeName {
10 const theme = useThemeName()
···4041function updateDocument(theme: ThemeName) {
42 // @ts-ignore web only
43- if (isWeb && typeof window !== 'undefined') {
44 // @ts-ignore web only
45 const html = window.document.documentElement
46 // @ts-ignore web only
···2import {type ColorSchemeName, useColorScheme} from 'react-native'
3import {type ThemeName} from '@bsky.app/alf'
405import {useThemePrefs} from '#/state/shell'
6import {dark, dim, light} from '#/alf/themes'
7+import {IS_WEB} from '#/env'
89export function useColorModeTheme(): ThemeName {
10 const theme = useThemeName()
···4041function updateDocument(theme: ThemeName) {
42 // @ts-ignore web only
43+ if (IS_WEB && typeof window !== 'undefined') {
44 // @ts-ignore web only
45 const html = window.document.documentElement
46 // @ts-ignore web only
···1import {useCallback} from 'react'
2import {SystemBars} from 'react-native-edge-to-edge'
34-import {isIOS} from '#/platform/detection'
56/**
7 * If we're calling a system API like the image picker that opens a sheet
···9 */
10export function useSheetWrapper() {
11 return useCallback(async <T>(promise: Promise<T>): Promise<T> => {
12- if (isIOS) {
13 const entry = SystemBars.pushStackEntry({
14 style: {
15 statusBar: 'light',
···1import {useCallback} from 'react'
2import {SystemBars} from 'react-native-edge-to-edge'
34+import {IS_IOS} from '#/env'
56/**
7 * If we're calling a system API like the image picker that opens a sheet
···9 */
10export function useSheetWrapper() {
11 return useCallback(async <T>(promise: Promise<T>): Promise<T> => {
12+ if (IS_IOS) {
13 const entry = SystemBars.pushStackEntry({
14 style: {
15 statusBar: 'light',
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
10import {sanitizeDisplayName} from '#/lib/strings/display-names'
11-import {isNative} from '#/platform/detection'
12import {useModerationOpts} from '#/state/preferences/moderation-opts'
13import {useSession} from '#/state/session'
14import {atoms as a, useTheme, web} from '#/alf'
···18import {Newskie} from '#/components/icons/Newskie'
19import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'
20import {Text} from '#/components/Typography'
02122export function NewskieDialog({
23 profile,
···162 </StarterPackCard.Link>
163 ) : null}
164165- {isNative && (
166 <Button
167 label={_(msg`Close`)}
168 color="secondary"
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
10import {sanitizeDisplayName} from '#/lib/strings/display-names'
011import {useModerationOpts} from '#/state/preferences/moderation-opts'
12import {useSession} from '#/state/session'
13import {atoms as a, useTheme, web} from '#/alf'
···17import {Newskie} from '#/components/icons/Newskie'
18import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'
19import {Text} from '#/components/Typography'
20+import {IS_NATIVE} from '#/env'
2122export function NewskieDialog({
23 profile,
···162 </StarterPackCard.Link>
163 ) : null}
164165+ {IS_NATIVE && (
166 <Button
167 label={_(msg`Close`)}
168 color="secondary"
+3-3
src/components/PolicyUpdateOverlay/Overlay.tsx
···7import {LinearGradient} from 'expo-linear-gradient'
8import {utils} from '@bsky.app/alf'
910-import {isAndroid, isNative} from '#/platform/detection'
11import {useA11y} from '#/state/a11y'
12import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
13import {FocusScope} from '#/components/FocusScope'
14import {LockScroll} from '#/components/LockScroll'
01516const GUTTER = 24
17···80 a.z_20,
81 a.align_center,
82 !gtPhone && [a.justify_end, {minHeight: frame.height}],
83- isNative && [
84 {
85 paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
86 },
···109110 <FocusScope>
111 <View
112- accessible={isAndroid}
113 role="dialog"
114 aria-role="dialog"
115 aria-label={label}
···7import {LinearGradient} from 'expo-linear-gradient'
8import {utils} from '@bsky.app/alf'
9010import {useA11y} from '#/state/a11y'
11import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
12import {FocusScope} from '#/components/FocusScope'
13import {LockScroll} from '#/components/LockScroll'
14+import {IS_ANDROID, IS_NATIVE} from '#/env'
1516const GUTTER = 24
17···80 a.z_20,
81 a.align_center,
82 !gtPhone && [a.justify_end, {minHeight: frame.height}],
83+ IS_NATIVE && [
84 {
85 paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
86 },
···109110 <FocusScope>
111 <View
112+ accessible={IS_ANDROID}
113 role="dialog"
114 aria-role="dialog"
115 aria-label={label}
+2-2
src/components/PolicyUpdateOverlay/index.tsx
···1import {useEffect} from 'react'
2import {View} from 'react-native'
34-import {isIOS} from '#/platform/detection'
5import {atoms as a} from '#/alf'
6import {FullWindowOverlay} from '#/components/FullWindowOverlay'
7import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
8import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
9import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
01011export {Provider} from '#/components/PolicyUpdateOverlay/context'
12export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
···39 // setting a zIndex when using FullWindowOverlay on iOS
40 // means the taps pass straight through to the underlying content (???)
41 // so don't set it on iOS. FullWindowOverlay already does the job.
42- !isIOS && {zIndex: 9999},
43 ]}>
44 <Content state={state} />
45 </View>
···1import {useEffect} from 'react'
2import {View} from 'react-native'
304import {atoms as a} from '#/alf'
5import {FullWindowOverlay} from '#/components/FullWindowOverlay'
6import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
7import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
8import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
9+import {IS_IOS} from '#/env'
1011export {Provider} from '#/components/PolicyUpdateOverlay/context'
12export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
···39 // setting a zIndex when using FullWindowOverlay on iOS
40 // means the taps pass straight through to the underlying content (???)
41 // so don't set it on iOS. FullWindowOverlay already does the job.
42+ !IS_IOS && {zIndex: 9999},
43 ]}>
44 <Content state={state} />
45 </View>
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isAndroid} from '#/platform/detection'
7import {useA11y} from '#/state/a11y'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonText} from '#/components/Button'
···12import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
13import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
14import {Text} from '#/components/Typography'
01516export function Content({state}: {state: PolicyUpdateState}) {
17 const t = useTheme()
···56 size: 'small',
57 } as const
5859- const label = isAndroid
60 ? _(
61 msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
62 )
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import {useA11y} from '#/state/a11y'
7import {atoms as a, useTheme} from '#/alf'
8import {Button, ButtonText} from '#/components/Button'
···11import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
12import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
13import {Text} from '#/components/Typography'
14+import {IS_ANDROID} from '#/env'
1516export function Content({state}: {state: PolicyUpdateState}) {
17 const t = useTheme()
···56 size: 'small',
57 } as const
5859+ const label = IS_ANDROID
60 ? _(
61 msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
62 )
···26 type EmbedPlayerParams,
27 getPlayerAspect,
28} from '#/lib/strings/embed-player'
29-import {isNative} from '#/platform/detection'
30import {useExternalEmbedsPrefs} from '#/state/preferences'
31import {EventStopper} from '#/view/com/util/EventStopper'
32import {atoms as a, useTheme} from '#/alf'
···34import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
35import {Fill} from '#/components/Fill'
36import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
03738interface ShouldStartLoadRequest {
39 url: string
···148 const {height: winHeight, width: winWidth} = windowDims
149150 // Get the proper screen height depending on what is going on
151- const realWinHeight = isNative // If it is native, we always want the larger number
152 ? winHeight > winWidth
153 ? winHeight
154 : winWidth
···26 type EmbedPlayerParams,
27 getPlayerAspect,
28} from '#/lib/strings/embed-player'
029import {useExternalEmbedsPrefs} from '#/state/preferences'
30import {EventStopper} from '#/view/com/util/EventStopper'
31import {atoms as a, useTheme} from '#/alf'
···33import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
34import {Fill} from '#/components/Fill'
35import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
36+import {IS_NATIVE} from '#/env'
3738interface ShouldStartLoadRequest {
39 url: string
···148 const {height: winHeight, width: winWidth} = windowDims
149150 // Get the proper screen height depending on what is going on
151+ const realWinHeight = IS_NATIVE // If it is native, we always want the larger number
152 ? winHeight > winWidth
153 ? winHeight
154 : winWidth
+6-6
src/components/Post/Embed/ExternalEmbed/Gif.tsx
···13import {HITSLOP_20} from '#/lib/constants'
14import {clamp} from '#/lib/numbers'
15import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
16-import {isWeb} from '#/platform/detection'
17import {useAutoplayDisabled} from '#/state/preferences'
18import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
19import {atoms as a, useTheme} from '#/alf'
···22import * as Prompt from '#/components/Prompt'
23import {Text} from '#/components/Typography'
24import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
025import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
26import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
27···218 altContainer: {
219 backgroundColor: 'rgba(0, 0, 0, 0.75)',
220 borderRadius: 6,
221- paddingHorizontal: isWeb ? 8 : 6,
222- paddingVertical: isWeb ? 6 : 3,
223 position: 'absolute',
224 // Related to margin/gap hack. This keeps the alt label in the same position
225 // on all platforms
226- right: isWeb ? 8 : 5,
227- bottom: isWeb ? 8 : 5,
228 zIndex: 2,
229 },
230 alt: {
231 color: 'white',
232- fontSize: isWeb ? 10 : 7,
233 fontWeight: '600',
234 },
235})
···13import {HITSLOP_20} from '#/lib/constants'
14import {clamp} from '#/lib/numbers'
15import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
016import {useAutoplayDisabled} from '#/state/preferences'
17import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
18import {atoms as a, useTheme} from '#/alf'
···21import * as Prompt from '#/components/Prompt'
22import {Text} from '#/components/Typography'
23import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
24+import {IS_WEB} from '#/env'
25import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
26import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
27···218 altContainer: {
219 backgroundColor: 'rgba(0, 0, 0, 0.75)',
220 borderRadius: 6,
221+ paddingHorizontal: IS_WEB ? 8 : 6,
222+ paddingVertical: IS_WEB ? 6 : 3,
223 position: 'absolute',
224 // Related to margin/gap hack. This keeps the alt label in the same position
225 // on all platforms
226+ right: IS_WEB ? 8 : 5,
227+ bottom: IS_WEB ? 8 : 5,
228 zIndex: 2,
229 },
230 alt: {
231 color: 'white',
232+ fontSize: IS_WEB ? 10 : 7,
233 fontWeight: '600',
234 },
235})
+2-2
src/components/Post/Embed/ExternalEmbed/index.tsx
···10import {shareUrl} from '#/lib/sharing'
11import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
12import {toNiceDomain} from '#/lib/strings/url-helpers'
13-import {isNative} from '#/platform/detection'
14import {useExternalEmbedsPrefs} from '#/state/preferences'
15import {atoms as a, useTheme} from '#/alf'
16import {Divider} from '#/components/Divider'
17import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
18import {Link} from '#/components/Link'
19import {Text} from '#/components/Typography'
020import {ExternalGif} from './ExternalGif'
21import {ExternalPlayer} from './ExternalPlayer'
22import {GifEmbed} from './Gif'
···53 }, [playHaptic, onOpen])
5455 const onShareExternal = useCallback(() => {
56- if (link.uri && isNative) {
57 playHaptic('Heavy')
58 shareUrl(link.uri)
59 }
···10import {shareUrl} from '#/lib/sharing'
11import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
12import {toNiceDomain} from '#/lib/strings/url-helpers'
013import {useExternalEmbedsPrefs} from '#/state/preferences'
14import {atoms as a, useTheme} from '#/alf'
15import {Divider} from '#/components/Divider'
16import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
17import {Link} from '#/components/Link'
18import {Text} from '#/components/Typography'
19+import {IS_NATIVE} from '#/env'
20import {ExternalGif} from './ExternalGif'
21import {ExternalPlayer} from './ExternalPlayer'
22import {GifEmbed} from './Gif'
···53 }, [playHaptic, onOpen])
5455 const onShareExternal = useCallback(() => {
56+ if (link.uri && IS_NATIVE) {
57 playHaptic('Heavy')
58 shareUrl(link.uri)
59 }
···10import {shareText, shareUrl} from '#/lib/sharing'
11import {toShareUrl} from '#/lib/strings/url-helpers'
12import {logger} from '#/logger'
13-import {isIOS} from '#/platform/detection'
14import {useProfileShadow} from '#/state/cache/profile-shadow'
15import {useSession} from '#/state/session'
16import * as Toast from '#/view/com/util/Toast'
···24import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
25import * as Menu from '#/components/Menu'
26import {useAgeAssurance} from '#/ageAssurance'
027import {useDevMode} from '#/storage/hooks/dev-mode'
28import {RecentChats} from './RecentChats'
29import {type ShareMenuItemsProps} from './ShareMenuItems.types'
···63 const onCopyLink = async () => {
64 logger.metric('share:press:copyLink', {}, {statsig: true})
65 const url = toShareUrl(href)
66- if (isIOS) {
67 // iOS only
68 await ExpoClipboard.setUrlAsync(url)
69 } else {
···10import {shareText, shareUrl} from '#/lib/sharing'
11import {toShareUrl} from '#/lib/strings/url-helpers'
12import {logger} from '#/logger'
013import {useProfileShadow} from '#/state/cache/profile-shadow'
14import {useSession} from '#/state/session'
15import * as Toast from '#/view/com/util/Toast'
···23import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
24import * as Menu from '#/components/Menu'
25import {useAgeAssurance} from '#/ageAssurance'
26+import {IS_IOS} from '#/env'
27import {useDevMode} from '#/storage/hooks/dev-mode'
28import {RecentChats} from './RecentChats'
29import {type ShareMenuItemsProps} from './ShareMenuItems.types'
···63 const onCopyLink = async () => {
64 logger.metric('share:press:copyLink', {}, {statsig: true})
65 const url = toShareUrl(href)
66+ if (IS_IOS) {
67 // iOS only
68 await ExpoClipboard.setUrlAsync(url)
69 } else {
···1import {View} from 'react-native'
23import {isTouchDevice} from '#/lib/browser'
4-import {isNative, isWeb} from '#/platform/detection'
5import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
067export function SubtleHover({
8 style,
···39 />
40 )
4142- if (isWeb && web) {
43 return isTouchDevice ? null : el
44- } else if (isNative && native) {
45 return el
46 }
47
···1import {View} from 'react-native'
23import {isTouchDevice} from '#/lib/browser'
04import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
5+import {IS_NATIVE, IS_WEB} from '#/env'
67export function SubtleHover({
8 style,
···39 />
40 )
4142+ if (IS_WEB && web) {
43 return isTouchDevice ? null : el
44+ } else if (IS_NATIVE && native) {
45 return el
46 }
47
+3-3
src/components/WhoCanReply.tsx
···18import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
20import {logger} from '#/logger'
21-import {isNative} from '#/platform/detection'
22import {
23 type ThreadgateAllowUISetting,
24 threadgateViewToAllowUISetting,
···37import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
38import {InlineLinkText} from '#/components/Link'
39import {Text} from '#/components/Typography'
040import * as bsky from '#/types/bsky'
4142interface WhoCanReplyProps {
···86 : _(msg`Some people can reply`)
8788 const onPressOpen = () => {
89- if (isNative && Keyboard.isVisible()) {
90 Keyboard.dismiss()
91 }
92 if (isThreadAuthor) {
···229 embeddingDisabled={embeddingDisabled}
230 />
231 </View>
232- {isNative && (
233 <Button
234 label={_(msg`Close`)}
235 onPress={() => control.close()}
···18import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
20import {logger} from '#/logger'
021import {
22 type ThreadgateAllowUISetting,
23 threadgateViewToAllowUISetting,
···36import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
37import {InlineLinkText} from '#/components/Link'
38import {Text} from '#/components/Typography'
39+import {IS_NATIVE} from '#/env'
40import * as bsky from '#/types/bsky'
4142interface WhoCanReplyProps {
···86 : _(msg`Some people can reply`)
8788 const onPressOpen = () => {
89+ if (IS_NATIVE && Keyboard.isVisible()) {
90 Keyboard.dismiss()
91 }
92 if (isThreadAuthor) {
···229 embeddingDisabled={embeddingDisabled}
230 />
231 </View>
232+ {IS_NATIVE && (
233 <Button
234 label={_(msg`Close`)}
235 onPress={() => control.close()}
···18import {cleanError} from '#/lib/strings/errors'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
21-import {isWeb} from '#/platform/detection'
22import {updateProfileShadow} from '#/state/cache/profile-shadow'
23import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
24import {useAgent} from '#/state/session'
···37import {Loader} from '#/components/Loader'
38import * as ProfileCard from '#/components/ProfileCard'
39import {Text} from '#/components/Typography'
040import type * as bsky from '#/types/bsky'
4142export function SubscribeProfileDialog({
···195 }
196 } else {
197 // on web, a disabled save button feels more natural than a massive close button
198- if (isWeb) {
199 return {
200 label: _(msg`Save changes`),
201 color: 'secondary',
···18import {cleanError} from '#/lib/strings/errors'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
021import {updateProfileShadow} from '#/state/cache/profile-shadow'
22import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
23import {useAgent} from '#/state/session'
···36import {Loader} from '#/components/Loader'
37import * as ProfileCard from '#/components/ProfileCard'
38import {Text} from '#/components/Typography'
39+import {IS_WEB} from '#/env'
40import type * as bsky from '#/types/bsky'
4142export function SubscribeProfileDialog({
···195 }
196 } else {
197 // on web, a disabled save button feels more natural than a massive close button
198+ if (IS_WEB) {
199 return {
200 label: _(msg`Save changes`),
201 color: 'secondary',
···56import {retry} from '#/lib/async/retry'
7import {wait} from '#/lib/async/wait'
8-import {isNative} from '#/platform/detection'
9import {useAgent} from '#/state/session'
10import {atoms as a, useTheme, web} from '#/alf'
11import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···18import {Text} from '#/components/Typography'
19import {refetchAgeAssuranceServerState} from '#/ageAssurance'
20import {logger} from '#/ageAssurance'
02122export type AgeAssuranceRedirectDialogState = {
23 result: 'success' | 'unknown'
···166 </Trans>
167 </Text>
168169- {isNative && (
170 <View style={[a.w_full, a.pt_lg]}>
171 <Button
172 label={_(msg`Close`)}
···225 )}
226 </Text>
227228- {error && isNative && (
229 <View style={[a.w_full, a.pt_lg]}>
230 <Button
231 label={_(msg`Close`)}
···56import {retry} from '#/lib/async/retry'
7import {wait} from '#/lib/async/wait'
08import {useAgent} from '#/state/session'
9import {atoms as a, useTheme, web} from '#/alf'
10import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···17import {Text} from '#/components/Typography'
18import {refetchAgeAssuranceServerState} from '#/ageAssurance'
19import {logger} from '#/ageAssurance'
20+import {IS_NATIVE} from '#/env'
2122export type AgeAssuranceRedirectDialogState = {
23 result: 'success' | 'unknown'
···166 </Trans>
167 </Text>
168169+ {IS_NATIVE && (
170 <View style={[a.w_full, a.pt_lg]}>
171 <Button
172 label={_(msg`Close`)}
···225 )}
226 </Text>
227228+ {error && IS_NATIVE && (
229 <View style={[a.w_full, a.pt_lg]}>
230 <Button
231 label={_(msg`Close`)}
+2-2
src/components/contacts/FindContactsBannerNUX.tsx
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGate} from '#/lib/statsig/statsig'
10import {logger} from '#/logger'
11-import {isWeb} from '#/platform/detection'
12import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
13import {atoms as a, useTheme} from '#/alf'
14import {Button} from '#/components/Button'
15import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
16import {Text} from '#/components/Typography'
017import {Link} from '../Link'
18import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
19···92 const gate = useGate()
9394 const visible = useMemo(() => {
95- if (isWeb) return false
96 if (hidden) return false
97 if (nux && nux.completed) return false
98 if (!isFeatureEnabled) return false
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGate} from '#/lib/statsig/statsig'
10import {logger} from '#/logger'
011import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
12import {atoms as a, useTheme} from '#/alf'
13import {Button} from '#/components/Button'
14import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
15import {Text} from '#/components/Typography'
16+import {IS_WEB} from '#/env'
17import {Link} from '../Link'
18import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
19···92 const gate = useGate()
9394 const visible = useMemo(() => {
95+ if (IS_WEB) return false
96 if (hidden) return false
97 if (nux && nux.completed) return false
98 if (!isFeatureEnabled) return false
+3-3
src/components/contacts/components/OTPInput.tsx
···9import {useLingui} from '@lingui/react'
1011import {mergeRefs} from '#/lib/merge-refs'
12-import {isAndroid, isIOS} from '#/platform/detection'
13import {atoms as a, ios, platform, useTheme} from '#/alf'
14import {useInteractionState} from '#/components/hooks/useInteractionState'
15import {Text} from '#/components/Typography'
01617export function OTPInput({
18 label,
···95 <TextInput
96 // SMS autofill is borked on iOS if you open the keyboard immediately -sfn
97 onLayout={ios(() => setTimeout(() => innerRef.current?.focus(), 100))}
98- autoFocus={isAndroid}
99 accessible
100 accessibilityLabel={label}
101 accessibilityHint=""
···135 android: {opacity: 0},
136 }),
137 ]}
138- caretHidden={isIOS}
139 clearTextOnFocus
140 />
141 </Pressable>
···9import {useLingui} from '@lingui/react'
1011import {mergeRefs} from '#/lib/merge-refs'
012import {atoms as a, ios, platform, useTheme} from '#/alf'
13import {useInteractionState} from '#/components/hooks/useInteractionState'
14import {Text} from '#/components/Typography'
15+import {IS_ANDROID, IS_IOS} from '#/env'
1617export function OTPInput({
18 label,
···95 <TextInput
96 // SMS autofill is borked on iOS if you open the keyboard immediately -sfn
97 onLayout={ios(() => setTimeout(() => innerRef.current?.focus(), 100))}
98+ autoFocus={IS_ANDROID}
99 accessible
100 accessibilityLabel={label}
101 accessibilityHint=""
···135 android: {opacity: 0},
136 }),
137 ]}
138+ caretHidden={IS_IOS}
139 clearTextOnFocus
140 />
141 </Pressable>
···6import {wait} from '#/lib/async/wait'
7import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError'
8import {logger} from '#/logger'
9-import {isWeb} from '#/platform/detection'
10import {atoms as a, useTheme, web} from '#/alf'
11import {Admonition} from '#/components/Admonition'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···14import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
15import {Loader} from '#/components/Loader'
16import {Text} from '#/components/Typography'
017import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation'
1819export type Props = {
···138 disabled={isRequesting}
139 label={_(msg`Allow location access`)}
140 onPress={onPressConfirm}
141- size={isWeb ? 'small' : 'large'}
142 color="primary">
143 <ButtonIcon icon={isRequesting ? Loader : LocationIcon} />
144 <ButtonText>
···147 </Button>
148 )}
149150- {!isWeb && (
151 <Button
152 label={_(msg`Cancel`)}
153 onPress={() => close()}
154- size={isWeb ? 'small' : 'large'}
155 color="secondary">
156 <ButtonText>
157 <Trans>Cancel</Trans>
···6import {wait} from '#/lib/async/wait'
7import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError'
8import {logger} from '#/logger'
09import {atoms as a, useTheme, web} from '#/alf'
10import {Admonition} from '#/components/Admonition'
11import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···13import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
14import {Loader} from '#/components/Loader'
15import {Text} from '#/components/Typography'
16+import {IS_WEB} from '#/env'
17import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation'
1819export type Props = {
···138 disabled={isRequesting}
139 label={_(msg`Allow location access`)}
140 onPress={onPressConfirm}
141+ size={IS_WEB ? 'small' : 'large'}
142 color="primary">
143 <ButtonIcon icon={isRequesting ? Loader : LocationIcon} />
144 <ButtonText>
···147 </Button>
148 )}
149150+ {!IS_WEB && (
151 <Button
152 label={_(msg`Cancel`)}
153 onPress={() => close()}
154+ size={IS_WEB ? 'small' : 'large'}
155 color="secondary">
156 <ButtonText>
157 <Trans>Cancel</Trans>
+3-3
src/components/dialogs/GifSelect.tsx
···1314import {logEvent} from '#/lib/statsig/statsig'
15import {cleanError} from '#/lib/strings/errors'
16-import {isWeb} from '#/platform/detection'
17import {
18 type Gif,
19 tenorUrlToBskyGifUrl,
···31import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
32import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
33import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
03435export function GifSelectDialog({
36 controlRef,
···149 a.pb_sm,
150 t.atoms.bg,
151 ]}>
152- {!gtMobile && isWeb && (
153 <Button
154 size="small"
155 variant="ghost"
···161 </Button>
162 )}
163164- <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}>
165 <TextField.Icon icon={Search} />
166 <TextField.Input
167 label={_(msg`Search GIFs`)}
···1314import {logEvent} from '#/lib/statsig/statsig'
15import {cleanError} from '#/lib/strings/errors'
016import {
17 type Gif,
18 tenorUrlToBskyGifUrl,
···30import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
31import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
32import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
33+import {IS_WEB} from '#/env'
3435export function GifSelectDialog({
36 controlRef,
···149 a.pb_sm,
150 t.atoms.bg,
151 ]}>
152+ {!gtMobile && IS_WEB && (
153 <Button
154 size="small"
155 variant="ghost"
···161 </Button>
162 )}
163164+ <TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
165 <TextField.Icon icon={Search} />
166 <TextField.Input
167 label={_(msg`Search GIFs`)}
+2-2
src/components/dialogs/InAppBrowserConsent.tsx
···4import {useLingui} from '@lingui/react'
56import {useOpenLink} from '#/lib/hooks/useOpenLink'
7-import {isWeb} from '#/platform/detection'
8import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
9import {atoms as a, useTheme} from '#/alf'
10import {Button, ButtonIcon, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
12import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight'
13import {Text} from '#/components/Typography'
014import {useGlobalDialogsControlContext} from './Context'
1516export function InAppBrowserConsentDialog() {
17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
1819- if (isWeb) return null
2021 return (
22 <Dialog.Outer
···4import {useLingui} from '@lingui/react'
56import {useOpenLink} from '#/lib/hooks/useOpenLink'
07import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonIcon, ButtonText} from '#/components/Button'
10import * as Dialog from '#/components/Dialog'
11import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight'
12import {Text} from '#/components/Typography'
13+import {IS_WEB} from '#/env'
14import {useGlobalDialogsControlContext} from './Context'
1516export function InAppBrowserConsentDialog() {
17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
1819+ if (IS_WEB) return null
2021 return (
22 <Dialog.Outer
+2-2
src/components/dialogs/MutedWords.tsx
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
8-import {isNative} from '#/platform/detection'
9import {
10 usePreferencesQuery,
11 useRemoveMutedWordMutation,
···32import {Loader} from '#/components/Loader'
33import * as Prompt from '#/components/Prompt'
34import {Text} from '#/components/Typography'
03536const ONE_DAY = 24 * 60 * 60 * 1000
37···406 )}
407 </View>
408409- {isNative && <View style={{height: 20}} />}
410 </View>
411412 <Dialog.Close />
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
08import {
9 usePreferencesQuery,
10 useRemoveMutedWordMutation,
···31import {Loader} from '#/components/Loader'
32import * as Prompt from '#/components/Prompt'
33import {Text} from '#/components/Typography'
34+import {IS_NATIVE} from '#/env'
3536const ONE_DAY = 24 * 60 * 60 * 1000
37···406 )}
407 </View>
408409+ {IS_NATIVE && <View style={{height: 20}} />}
410 </View>
411412 <Dialog.Close />
···67import {urls} from '#/lib/constants'
8import {logger} from '#/logger'
9-import {isNative} from '#/platform/detection'
10import {atoms as a, useBreakpoints, useTheme} from '#/alf'
11import {Button, ButtonText} from '#/components/Button'
12import * as Dialog from '#/components/Dialog'
···15import {VerifierCheck} from '#/components/icons/VerifierCheck'
16import {Link} from '#/components/Link'
17import {Span, Text} from '#/components/Typography'
01819export function InitialVerificationAnnouncement() {
20 const t = useTheme()
···173 <Trans>Read blog post</Trans>
174 </ButtonText>
175 </Link>
176- {isNative && (
177 <Button
178 label={_(msg`Close`)}
179 size="small"
···67import {urls} from '#/lib/constants'
8import {logger} from '#/logger'
09import {atoms as a, useBreakpoints, useTheme} from '#/alf'
10import {Button, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
···14import {VerifierCheck} from '#/components/icons/VerifierCheck'
15import {Link} from '#/components/Link'
16import {Span, Text} from '#/components/Typography'
17+import {IS_NATIVE} from '#/env'
1819export function InitialVerificationAnnouncement() {
20 const t = useTheme()
···173 <Trans>Read blog post</Trans>
174 </ButtonText>
175 </Link>
176+ {IS_NATIVE && (
177 <Button
178 label={_(msg`Close`)}
179 size="small"
+5-5
src/components/dialogs/nuxs/LiveNowBetaDialog.tsx
···5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
78-import {isWeb} from '#/platform/detection'
9import {atoms as a, select, useTheme, utils, web} from '#/alf'
10import {Button, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
···16} from '#/components/dialogs/nuxs/utils'
17import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
18import {Text} from '#/components/Typography'
019import {IS_E2E} from '#/env'
2021export const enabled = createIsEnabledCheck(props => {
···72 a.overflow_hidden,
73 {
74 gap: 16,
75- paddingTop: isWeb ? 24 : 40,
76 borderTopLeftRadius: a.rounded_md.borderRadius,
77 borderTopRightRadius: a.rounded_md.borderRadius,
78 },
···116 borderRadius: 24,
117 aspectRatio: 652 / 211,
118 },
119- isWeb
120 ? [
121 {
122 boxShadow: `0px 10px 15px -3px ${shadowColor}`,
···163 a.font_bold,
164 a.text_center,
165 {
166- fontSize: isWeb ? 28 : 32,
167 maxWidth: 360,
168 },
169 ]}>
···186 </Text>
187 </View>
188189- {!isWeb && (
190 <Button
191 label={_(msg`Close`)}
192 size="large"
···5import {msg, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
708import {atoms as a, select, useTheme, utils, web} from '#/alf'
9import {Button, ButtonText} from '#/components/Button'
10import * as Dialog from '#/components/Dialog'
···15} from '#/components/dialogs/nuxs/utils'
16import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
17import {Text} from '#/components/Typography'
18+import {IS_WEB} from '#/env'
19import {IS_E2E} from '#/env'
2021export const enabled = createIsEnabledCheck(props => {
···72 a.overflow_hidden,
73 {
74 gap: 16,
75+ paddingTop: IS_WEB ? 24 : 40,
76 borderTopLeftRadius: a.rounded_md.borderRadius,
77 borderTopRightRadius: a.rounded_md.borderRadius,
78 },
···116 borderRadius: 24,
117 aspectRatio: 652 / 211,
118 },
119+ IS_WEB
120 ? [
121 {
122 boxShadow: `0px 10px 15px -3px ${shadowColor}`,
···163 a.font_bold,
164 a.text_center,
165 {
166+ fontSize: IS_WEB ? 28 : 32,
167 maxWidth: 360,
168 },
169 ]}>
···186 </Text>
187 </View>
188189+ {!IS_WEB && (
190 <Button
191 label={_(msg`Close`)}
192 size="large"
+1-1
src/components/dms/ActionsWrapper.tsx
···21 <MessageContextMenu message={message}>
22 {trigger =>
23 // will always be true, since this file is platform split
24- trigger.isNative && (
25 <View style={[a.flex_1, a.relative]}>
26 <View
27 style={[
···21 <MessageContextMenu message={message}>
22 {trigger =>
23 // will always be true, since this file is platform split
24+ trigger.IS_NATIVE && (
25 <View style={[a.flex_1, a.relative]}>
26 <View
27 style={[
···4import {useLingui} from '@lingui/react'
56import {HITSLOP_10} from '#/lib/constants'
7-import {isNative} from '#/platform/detection'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonIcon} from '#/components/Button'
10import * as TextField from '#/components/forms/TextField'
11import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
12import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
01314type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
15 label?: TextField.InputProps['label']
···36 placeholder={_(msg`Search`)}
37 returnKeyType="search"
38 keyboardAppearance={t.scheme}
39- selectTextOnFocus={isNative}
40 autoFocus={false}
41 accessibilityRole="search"
42 autoCorrect={false}
···4import {useLingui} from '@lingui/react'
56import {HITSLOP_10} from '#/lib/constants'
07import {atoms as a, useTheme} from '#/alf'
8import {Button, ButtonIcon} from '#/components/Button'
9import * as TextField from '#/components/forms/TextField'
10import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
11import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
12+import {IS_NATIVE} from '#/env'
1314type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
15 label?: TextField.InputProps['label']
···36 placeholder={_(msg`Search`)}
37 returnKeyType="search"
38 keyboardAppearance={t.scheme}
39+ selectTextOnFocus={IS_NATIVE}
40 autoFocus={false}
41 accessibilityRole="search"
42 autoCorrect={false}
+2-2
src/components/forms/Toggle/index.tsx
···1011import {HITSLOP_10} from '#/lib/constants'
12import {useHaptics} from '#/lib/haptics'
13-import {isNative} from '#/platform/detection'
14import {
15 atoms as a,
16 native,
···22import {useInteractionState} from '#/components/hooks/useInteractionState'
23import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
24import {Text} from '#/components/Typography'
02526export * from './Panel'
27···562 )
563}
564565-export const Platform = isNative ? Switch : Checkbox
···1011import {HITSLOP_10} from '#/lib/constants'
12import {useHaptics} from '#/lib/haptics'
013import {
14 atoms as a,
15 native,
···21import {useInteractionState} from '#/components/hooks/useInteractionState'
22import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
23import {Text} from '#/components/Typography'
24+import {IS_NATIVE} from '#/env'
2526export * from './Panel'
27···562 )
563}
564565+export const Platform = IS_NATIVE ? Switch : Checkbox
+2-2
src/components/hooks/useFullscreen.ts
···7} from 'react'
89import {isFirefox, isSafari} from '#/lib/browser'
10-import {isWeb} from '#/platform/detection'
1112function fullscreenSubscribe(onChange: () => void) {
13 document.addEventListener('fullscreenchange', onChange)
···15}
1617export function useFullscreen(ref?: React.RefObject<HTMLElement | null>) {
18- if (!isWeb) throw new Error("'useFullscreen' is a web-only hook")
19 const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
20 Boolean(document.fullscreenElement),
21 )
···7} from 'react'
89import {isFirefox, isSafari} from '#/lib/browser'
10+import {IS_WEB} from '#/env'
1112function fullscreenSubscribe(onChange: () => void) {
13 document.addEventListener('fullscreenchange', onChange)
···15}
1617export function useFullscreen(ref?: React.RefObject<HTMLElement | null>) {
18+ if (!IS_WEB) throw new Error("'useFullscreen' is a web-only hook")
19 const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
20 Boolean(document.fullscreenElement),
21 )
···4 createStarterPackLinkFromAndroidReferrer,
5 httpStarterPackUriToAtUri,
6} from '#/lib/strings/starter-pack'
7-import {isAndroid} from '#/platform/detection'
8import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
9import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
010import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
1112export function useStarterPackEntry() {
···32 ;(async () => {
33 let uri: string | null | undefined
3435- if (isAndroid) {
36 const res = await Referrer.getGooglePlayReferrerInfoAsync()
3738 if (res && res.installReferrer) {
···4 createStarterPackLinkFromAndroidReferrer,
5 httpStarterPackUriToAtUri,
6} from '#/lib/strings/starter-pack'
07import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
8import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
9+import {IS_ANDROID} from '#/env'
10import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
1112export function useStarterPackEntry() {
···32 ;(async () => {
33 let uri: string | null | undefined
3435+ if (IS_ANDROID) {
36 const res = await Referrer.getGooglePlayReferrerInfoAsync()
3738 if (res && res.installReferrer) {
+2-2
src/components/hooks/useWelcomeModal.ts
···1import {useEffect, useState} from 'react'
23-import {isWeb} from '#/platform/detection'
4import {useSession} from '#/state/session'
056export function useWelcomeModal() {
7 const {hasSession} = useSession()
···22 // 2. We're on the web (this is a web-only feature)
23 // 3. We're on the homepage (path is '/' or '/home')
24 // 4. User hasn't actively closed the modal in this session
25- if (isWeb && !hasSession && typeof window !== 'undefined') {
26 const currentPath = window.location.pathname
27 const isHomePage = currentPath === '/'
28 const hasUserClosedModal =
···1import {useEffect, useState} from 'react'
203import {useSession} from '#/state/session'
4+import {IS_WEB} from '#/env'
56export function useWelcomeModal() {
7 const {hasSession} = useSession()
···22 // 2. We're on the web (this is a web-only feature)
23 // 3. We're on the homepage (path is '/' or '/home')
24 // 4. User hasn't actively closed the modal in this session
25+ if (IS_WEB && !hasSession && typeof window !== 'undefined') {
26 const currentPath = window.location.pathname
27 const isHomePage = currentPath === '/'
28 const hasUserClosedModal =
+2-2
src/components/images/AutoSizedImage.tsx
···11import {useLingui} from '@lingui/react'
1213import {type Dimensions} from '#/lib/media/types'
14-import {isNative} from '#/platform/detection'
15import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
16import {atoms as a, useTheme} from '#/alf'
17import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
18import {MediaInsetBorder} from '#/components/MediaInsetBorder'
19import {Text} from '#/components/Typography'
02021export function ConstrainedImage({
22 aspectRatio,
···35 * the height of the image.
36 */
37 const outerAspectRatio = useMemo<DimensionValue>(() => {
38- const ratio = isNative
39 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
40 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box
41 return `${ratio * 100}%`
···11import {useLingui} from '@lingui/react'
1213import {type Dimensions} from '#/lib/media/types'
014import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
15import {atoms as a, useTheme} from '#/alf'
16import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
17import {MediaInsetBorder} from '#/components/MediaInsetBorder'
18import {Text} from '#/components/Typography'
19+import {IS_NATIVE} from '#/env'
2021export function ConstrainedImage({
22 aspectRatio,
···35 * the height of the image.
36 */
37 const outerAspectRatio = useMemo<DimensionValue>(() => {
38+ const ratio = IS_NATIVE
39 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
40 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box
41 return `${ratio * 100}%`
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isNative} from '#/platform/detection'
7import {useAgent, useSession} from '#/state/session'
8import {atoms as a, useBreakpoints, useTheme} from '#/alf'
9import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···15import {useIntentDialogs} from '#/components/intents/IntentDialogs'
16import {Loader} from '#/components/Loader'
17import {Text} from '#/components/Typography'
01819export function VerifyEmailIntentDialog() {
20 const {verifyEmailDialogControl: control} = useIntentDialogs()
···68 <Loader size="xl" fill={t.atoms.text_contrast_low.color} />
69 </View>
70 ) : status === 'success' ? (
71- <View style={[a.gap_sm, isNative && a.pb_xl]}>
72 <Text style={[a.font_bold, a.text_2xl]}>
73 <Trans>Email Verified</Trans>
74 </Text>
···93 </Text>
94 </View>
95 ) : (
96- <View style={[a.gap_sm, isNative && a.pb_xl]}>
97 <Text style={[a.font_bold, a.text_2xl]}>
98 <Trans>Email Resent</Trans>
99 </Text>
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import {useAgent, useSession} from '#/state/session'
7import {atoms as a, useBreakpoints, useTheme} from '#/alf'
8import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···14import {useIntentDialogs} from '#/components/intents/IntentDialogs'
15import {Loader} from '#/components/Loader'
16import {Text} from '#/components/Typography'
17+import {IS_NATIVE} from '#/env'
1819export function VerifyEmailIntentDialog() {
20 const {verifyEmailDialogControl: control} = useIntentDialogs()
···68 <Loader size="xl" fill={t.atoms.text_contrast_low.color} />
69 </View>
70 ) : status === 'success' ? (
71+ <View style={[a.gap_sm, IS_NATIVE && a.pb_xl]}>
72 <Text style={[a.font_bold, a.text_2xl]}>
73 <Trans>Email Verified</Trans>
74 </Text>
···93 </Text>
94 </View>
95 ) : (
96+ <View style={[a.gap_sm, IS_NATIVE && a.pb_xl]}>
97 <Text style={[a.font_bold, a.text_2xl]}>
98 <Trans>Email Resent</Trans>
99 </Text>
+2-2
src/components/moderation/LabelsOnMeDialog.tsx
···12import {makeProfileLink} from '#/lib/routes/links'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {logger} from '#/logger'
15-import {isAndroid} from '#/platform/detection'
16import {useAgent, useSession} from '#/state/session'
17import * as Toast from '#/view/com/util/Toast'
18import {atoms as a, useBreakpoints, useTheme} from '#/alf'
···20import * as Dialog from '#/components/Dialog'
21import {InlineLinkText} from '#/components/Link'
22import {Text} from '#/components/Typography'
023import {Admonition} from '../Admonition'
24import {Divider} from '../Divider'
25import {Loader} from '../Loader'
···344 {isPending && <ButtonIcon icon={Loader} />}
345 </Button>
346 </View>
347- {isAndroid && <View style={{height: 300}} />}
348 </>
349 )
350}
···12import {makeProfileLink} from '#/lib/routes/links'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {logger} from '#/logger'
015import {useAgent, useSession} from '#/state/session'
16import * as Toast from '#/view/com/util/Toast'
17import {atoms as a, useBreakpoints, useTheme} from '#/alf'
···19import * as Dialog from '#/components/Dialog'
20import {InlineLinkText} from '#/components/Link'
21import {Text} from '#/components/Typography'
22+import {IS_ANDROID} from '#/env'
23import {Admonition} from '../Admonition'
24import {Divider} from '../Divider'
25import {Loader} from '../Loader'
···344 {isPending && <ButtonIcon icon={Loader} />}
345 </Button>
346 </View>
347+ {IS_ANDROID && <View style={{height: 300}} />}
348 </>
349 )
350}
···13 * The short commit hash and environment of the current bundle.
14 */
15export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
0000000000000
···3import {useLingui} from '@lingui/react'
45import {useCleanError} from '#/lib/hooks/useCleanError'
6-import {isNative} from '#/platform/detection'
7import {atoms as a, web} from '#/alf'
8import {Admonition} from '#/components/Admonition'
9import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···11import {Loader} from '#/components/Loader'
12import * as Toast from '#/components/Toast'
13import {Span, Text} from '#/components/Typography'
014import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
15import {
16 type LiveEventFeed,
···146 </ButtonText>
147 {isHidingAllFeeds && <ButtonIcon icon={Loader} />}
148 </Button>
149- {isNative && (
150 <Button
151 label={_(msg`Cancel`)}
152 size="large"
···3import {useLingui} from '@lingui/react'
45import {useCleanError} from '#/lib/hooks/useCleanError'
06import {atoms as a, web} from '#/alf'
7import {Admonition} from '#/components/Admonition'
8import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···10import {Loader} from '#/components/Loader'
11import * as Toast from '#/components/Toast'
12import {Span, Text} from '#/components/Typography'
13+import {IS_NATIVE} from '#/env'
14import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
15import {
16 type LiveEventFeed,
···146 </ButtonText>
147 {isHidingAllFeeds && <ButtonIcon icon={Loader} />}
148 </Button>
149+ {IS_NATIVE && (
150 <Button
151 label={_(msg`Cancel`)}
152 size="large"
+2-2
src/features/liveEvents/preferences.ts
···3import {useMutation, useQueryClient} from '@tanstack/react-query'
45import {logger} from '#/logger'
6-import {isWeb} from '#/platform/detection'
7import {
8 preferencesQueryKey,
9 usePreferencesQuery,
10} from '#/state/queries/preferences'
11import {useAgent} from '#/state/session'
012import * as env from '#/env'
13import {
14 type LiveEventFeed,
···41 const agent = useAgent()
4243 useEffect(() => {
44- if (env.IS_DEV && isWeb && typeof window !== 'undefined') {
45 // @ts-ignore
46 window.__updateLiveEventPreferences = async (
47 action: LiveEventPreferencesAction,
···3import {useMutation, useQueryClient} from '@tanstack/react-query'
45import {logger} from '#/logger'
06import {
7 preferencesQueryKey,
8 usePreferencesQuery,
9} from '#/state/queries/preferences'
10import {useAgent} from '#/state/session'
11+import {IS_WEB} from '#/env'
12import * as env from '#/env'
13import {
14 type LiveEventFeed,
···41 const agent = useAgent()
4243 useEffect(() => {
44+ if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') {
45 // @ts-ignore
46 window.__updateLiveEventPreferences = async (
47 action: LiveEventPreferencesAction,
+2-2
src/geolocation/device.ts
···3import * as Location from 'expo-location'
4import {createPermissionHook} from 'expo-modules-core'
56-import {isNative} from '#/platform/detection'
7import * as debug from '#/geolocation/debug'
8import {logger} from '#/geolocation/logger'
9import {type Geolocation} from '#/geolocation/types'
···118 const synced = useRef(false)
119 const [status] = useForegroundPermissions()
120 useEffect(() => {
121- if (!isNative) return
122123 async function get() {
124 // no need to set this more than once per session
···3import * as Location from 'expo-location'
4import {createPermissionHook} from 'expo-modules-core'
56+import {IS_NATIVE} from '#/env'
7import * as debug from '#/geolocation/debug'
8import {logger} from '#/geolocation/logger'
9import {type Geolocation} from '#/geolocation/types'
···118 const synced = useRef(false)
119 const [status] = useForegroundPermissions()
120 useEffect(() => {
121+ if (!IS_NATIVE) return
122123 async function get() {
124 // no need to set this more than once per session
+2-2
src/geolocation/util.ts
···1import {type LocationGeocodedAddress} from 'expo-location'
23-import {isAndroid} from '#/platform/detection'
4import {logger} from '#/geolocation/logger'
5import {type Geolocation} from '#/geolocation/types'
6···81 /*
82 * Android doesn't give us ISO 3166-2 short codes. We need these for US
83 */
84- if (isAndroid) {
85 if (region && isoCountryCode === 'US') {
86 /*
87 * We need short codes for US states. If we can't remap it, just drop it
···1import {type LocationGeocodedAddress} from 'expo-location'
23+import {IS_ANDROID} from '#/env'
4import {logger} from '#/geolocation/logger'
5import {type Geolocation} from '#/geolocation/types'
6···81 /*
82 * Android doesn't give us ISO 3166-2 short codes. We need these for US
83 */
84+ if (IS_ANDROID) {
85 if (region && isoCountryCode === 'US') {
86 /*
87 * We need short codes for US states. If we can't remap it, just drop it
+2-2
src/lib/api/feed/utils.ts
···1import {AtUri} from '@atproto/api'
23import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
4-import {isWeb} from '#/platform/detection'
5import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
067let debugTopics = ''
8-if (isWeb && typeof window !== 'undefined') {
9 const params = new URLSearchParams(window.location.search)
10 debugTopics = params.get('debug_topics') ?? ''
11}
···1import {AtUri} from '@atproto/api'
23import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants'
04import {type UsePreferencesQueryResponse} from '#/state/queries/preferences'
5+import {IS_WEB} from '#/env'
67let debugTopics = ''
8+if (IS_WEB && typeof window !== 'undefined') {
9 const params = new URLSearchParams(window.location.search)
10 debugTopics = params.get('debug_topics') ?? ''
11}
···13} from 'react-native-reanimated'
1415import {isTouchDevice} from '#/lib/browser'
16-import {isNative} from '#/platform/detection'
1718-const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1
1920const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
21
···13} from 'react-native-reanimated'
1415import {isTouchDevice} from '#/lib/browser'
16+import {IS_NATIVE} from '#/env'
1718+const DEFAULT_TARGET_SCALE = IS_NATIVE || isTouchDevice ? 0.98 : 1
1920const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
21
+3-3
src/lib/haptics.ts
···2import * as Device from 'expo-device'
3import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
45-import {isIOS, isWeb} from '#/platform/detection'
6import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
078export function useHaptics() {
9 const isHapticsDisabled = useHapticsDisabled()
1011 return React.useCallback(
12 (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
13- if (isHapticsDisabled || isWeb) {
14 return
15 }
1617 // Users said the medium impact was too strong on Android; see APP-537s
18- const style = isIOS
19 ? ImpactFeedbackStyle[strength]
20 : ImpactFeedbackStyle.Light
21 impactAsync(style)
···2import * as Device from 'expo-device'
3import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics'
405import {useHapticsDisabled} from '#/state/preferences/disable-haptics'
6+import {IS_IOS, IS_WEB} from '#/env'
78export function useHaptics() {
9 const isHapticsDisabled = useHapticsDisabled()
1011 return React.useCallback(
12 (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => {
13+ if (isHapticsDisabled || IS_WEB) {
14 return
15 }
1617 // Users said the medium impact was too strong on Android; see APP-537s
18+ const style = IS_IOS
19 ? ImpactFeedbackStyle[strength]
20 : ImpactFeedbackStyle.Light
21 impactAsync(style)
+2-2
src/lib/hooks/useAccountSwitcher.ts
···3import {useLingui} from '@lingui/react'
45import {logger} from '#/logger'
6-import {isWeb} from '#/platform/detection'
7import {type SessionAccount, useSessionApi} from '#/state/session'
8import {useLoggedOutViewControls} from '#/state/shell/logged-out'
9import * as Toast from '#/view/com/util/Toast'
010import {logEvent} from '../statsig/statsig'
11import {type LogEvents} from '../statsig/statsig'
12···28 try {
29 setPendingDid(account.did)
30 if (account.accessJwt) {
31- if (isWeb) {
32 // We're switching accounts, which remounts the entire app.
33 // On mobile, this gets us Home, but on the web we also need reset the URL.
34 // We can't change the URL via a navigate() call because the navigator
···3import {useLingui} from '@lingui/react'
45import {logger} from '#/logger'
06import {type SessionAccount, useSessionApi} from '#/state/session'
7import {useLoggedOutViewControls} from '#/state/shell/logged-out'
8import * as Toast from '#/view/com/util/Toast'
9+import {IS_WEB} from '#/env'
10import {logEvent} from '../statsig/statsig'
11import {type LogEvents} from '../statsig/statsig'
12···28 try {
29 setPendingDid(account.did)
30 if (account.accessJwt) {
31+ if (IS_WEB) {
32 // We're switching accounts, which remounts the entire app.
33 // On mobile, this gets us Home, but on the web we also need reset the URL.
34 // We can't change the URL via a navigate() call because the navigator
···9import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
10import {logger as notyLogger} from '#/lib/notifications/util'
11import {type NavigationProp} from '#/lib/routes/types'
12-import {isAndroid, isIOS} from '#/platform/detection'
13import {useCurrentConvoId} from '#/state/messages/current-convo-id'
14import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
15import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
···17import {useSession} from '#/state/session'
18import {useLoggedOutViewControls} from '#/state/shell/logged-out'
19import {useCloseAllActiveElements} from '#/state/util'
020import {resetToTab} from '#/Navigation'
21import {router} from '#/routes'
22···90 // channels allow for the mute/unmute functionality we want for the background
91 // handler.
92 useEffect(() => {
93- if (!isAndroid) return
94 // assign both chat notifications to a group
95 // NOTE: I don't think that it will retroactively move them into the group
96 // if the channels already exist. no big deal imo -sfn
···379 }
380381 const payload = (
382- isIOS ? e.request.trigger.payload : e.request.content.data
383 ) as NotificationPayload
384385 if (payload && payload.reason) {
···9import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
10import {logger as notyLogger} from '#/lib/notifications/util'
11import {type NavigationProp} from '#/lib/routes/types'
012import {useCurrentConvoId} from '#/state/messages/current-convo-id'
13import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
14import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
···16import {useSession} from '#/state/session'
17import {useLoggedOutViewControls} from '#/state/shell/logged-out'
18import {useCloseAllActiveElements} from '#/state/util'
19+import {IS_ANDROID, IS_IOS} from '#/env'
20import {resetToTab} from '#/Navigation'
21import {router} from '#/routes'
22···90 // channels allow for the mute/unmute functionality we want for the background
91 // handler.
92 useEffect(() => {
93+ if (!IS_ANDROID) return
94 // assign both chat notifications to a group
95 // NOTE: I don't think that it will retroactively move them into the group
96 // if the channels already exist. no big deal imo -sfn
···379 }
380381 const payload = (
382+ IS_IOS ? e.request.trigger.payload : e.request.content.data
383 ) as NotificationPayload
384385 if (payload && payload.reason) {
+4-4
src/lib/hooks/useOTAUpdates.ts
···1213import {isNetworkError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15-import {isAndroid, isIOS} from '#/platform/detection'
16import {IS_TESTFLIGHT} from '#/env'
1718const MINIMUM_MINIMIZE_TIME = 15 * 60e3
1920async function setExtraParams() {
21 await setExtraParamAsync(
22- isIOS ? 'ios-build-number' : 'android-build-number',
23 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
24 // This just ensures it gets passed as a string
25 `${nativeBuildVersion}`,
···3233async function setExtraParamsPullRequest(channel: string) {
34 await setExtraParamAsync(
35- isIOS ? 'ios-build-number' : 'android-build-number',
36 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
37 // This just ensures it gets passed as a string
38 `${nativeBuildVersion}`,
···198 // `maintainVisibleContentPosition`. See repro repo for more details:
199 // https://github.com/mozzius/ota-crash-repro
200 // Old Arch only - re-enable once we're on the New Archictecture! -sfn
201- if (isAndroid) return
202203 const subscription = AppState.addEventListener(
204 'change',
···1213import {isNetworkError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15+import {IS_ANDROID, IS_IOS} from '#/env'
16import {IS_TESTFLIGHT} from '#/env'
1718const MINIMUM_MINIMIZE_TIME = 15 * 60e3
1920async function setExtraParams() {
21 await setExtraParamAsync(
22+ IS_IOS ? 'ios-build-number' : 'android-build-number',
23 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
24 // This just ensures it gets passed as a string
25 `${nativeBuildVersion}`,
···3233async function setExtraParamsPullRequest(channel: string) {
34 await setExtraParamAsync(
35+ IS_IOS ? 'ios-build-number' : 'android-build-number',
36 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
37 // This just ensures it gets passed as a string
38 `${nativeBuildVersion}`,
···198 // `maintainVisibleContentPosition`. See repro repo for more details:
199 // https://github.com/mozzius/ota-crash-repro
200 // Old Arch only - re-enable once we're on the New Archictecture! -sfn
201+ if (IS_ANDROID) return
202203 const subscription = AppState.addEventListener(
204 'change',
+2-2
src/lib/hooks/useOpenLink.ts
···12 toNiceDomain,
13} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
15-import {isNative} from '#/platform/detection'
16import {useInAppBrowser} from '#/state/preferences/in-app-browser'
17import {useTheme} from '#/alf'
18import {useDialogContext} from '#/components/Dialog'
19import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
02021export function useOpenLink() {
22 const enabled = useInAppBrowser()
···41 }
42 }
4344- if (isNative && !url.startsWith('mailto:')) {
45 if (override === undefined && enabled === undefined) {
46 // consent dialog is a global dialog, and while it's possible to nest dialogs,
47 // the actual components need to be nested. sibling dialogs on iOS are not supported.
···12 toNiceDomain,
13} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
015import {useInAppBrowser} from '#/state/preferences/in-app-browser'
16import {useTheme} from '#/alf'
17import {useDialogContext} from '#/components/Dialog'
18import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
19+import {IS_NATIVE} from '#/env'
2021export function useOpenLink() {
22 const enabled = useInAppBrowser()
···41 }
42 }
4344+ if (IS_NATIVE && !url.startsWith('mailto:')) {
45 if (override === undefined && enabled === undefined) {
46 // consent dialog is a global dialog, and while it's possible to nest dialogs,
47 // the actual components need to be nested. sibling dialogs on iOS are not supported.
+3-3
src/lib/hooks/usePermissions.ts
···2import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
3import * as MediaLibrary from 'expo-media-library'
45-import {isWeb} from '#/platform/detection'
6import {Alert} from '#/view/com/util/Alert'
078const openPermissionAlert = (perm: string) => {
9 Alert.alert(
···26 const requestPhotoAccessIfNeeded = async () => {
27 // On the, we use <input type="file"> to produce a filepicker
28 // This does not need any permission granting.
29- if (isWeb) {
30 return true
31 }
32···55 const requestVideoAccessIfNeeded = async () => {
56 // On the, we use <input type="file"> to produce a filepicker
57 // This does not need any permission granting.
58- if (isWeb) {
59 return true
60 }
61
···2import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
3import * as MediaLibrary from 'expo-media-library'
405import {Alert} from '#/view/com/util/Alert'
6+import {IS_WEB} from '#/env'
78const openPermissionAlert = (perm: string) => {
9 Alert.alert(
···26 const requestPhotoAccessIfNeeded = async () => {
27 // On the, we use <input type="file"> to produce a filepicker
28 // This does not need any permission granting.
29+ if (IS_WEB) {
30 return true
31 }
32···55 const requestVideoAccessIfNeeded = async () => {
56 // On the, we use <input type="file"> to produce a filepicker
57 // This does not need any permission granting.
58+ if (IS_WEB) {
59 return true
60 }
61
+2-2
src/lib/hooks/useTranslate.ts
···2import * as IntentLauncher from 'expo-intent-launcher'
34import {getTranslatorLink} from '#/locale/helpers'
5-import {isAndroid} from '#/platform/detection'
6import {useOpenLink} from './useOpenLink'
78export function useTranslate() {
···11 return useCallback(
12 async (text: string, language: string) => {
13 const translateUrl = getTranslatorLink(text, language)
14- if (isAndroid) {
15 try {
16 // use getApplicationIconAsync to determine if the translate app is installed
17 if (
···2import * as IntentLauncher from 'expo-intent-launcher'
34import {getTranslatorLink} from '#/locale/helpers'
5+import {IS_ANDROID} from '#/env'
6import {useOpenLink} from './useOpenLink'
78export function useTranslate() {
···11 return useCallback(
12 async (text: string, language: string) => {
13 const translateUrl = getTranslatorLink(text, language)
14+ if (IS_ANDROID) {
15 try {
16 // use getApplicationIconAsync to determine if the translate app is installed
17 if (
···1819import {POST_IMG_MAX} from '#/lib/constants'
20import {logger} from '#/logger'
21-import {isAndroid, isIOS} from '#/platform/detection'
22import {type PickerImage} from './picker.shared'
23import {type Dimensions} from './types'
24···108109 // save
110 try {
111- if (isAndroid) {
112 // android triggers an annoying permission prompt if you try and move an image
113 // between albums. therefore, we need to either create the album with the image
114 // as the starting image, or put it directly into the album
···305}
306307function normalizePath(str: string, allPlatforms = false): string {
308- if (isAndroid || allPlatforms) {
309 if (!str.startsWith('file://')) {
310 return `file://${str}`
311 }
···328 type: string,
329) {
330 try {
331- if (isIOS) {
332 await withTempFile(filename, encoded, async tmpFileUrl => {
333 await Sharing.shareAsync(tmpFileUrl, {UTI: type})
334 })
···1819import {POST_IMG_MAX} from '#/lib/constants'
20import {logger} from '#/logger'
21+import {IS_ANDROID, IS_IOS} from '#/env'
22import {type PickerImage} from './picker.shared'
23import {type Dimensions} from './types'
24···108109 // save
110 try {
111+ if (IS_ANDROID) {
112 // android triggers an annoying permission prompt if you try and move an image
113 // between albums. therefore, we need to either create the album with the image
114 // as the starting image, or put it directly into the album
···305}
306307function normalizePath(str: string, allPlatforms = false): string {
308+ if (IS_ANDROID || allPlatforms) {
309 if (!str.startsWith('file://')) {
310 return `file://${str}`
311 }
···328 type: string,
329) {
330 try {
331+ if (IS_IOS) {
332 await withTempFile(filename, encoded, async tmpFileUrl => {
333 await Sharing.shareAsync(tmpFileUrl, {UTI: type})
334 })
+3-3
src/lib/media/picker.shared.ts
···5} from 'expo-image-picker'
6import {t} from '@lingui/macro'
78-import {isIOS, isWeb} from '#/platform/detection'
9import {type ImageMeta} from '#/state/gallery'
10import * as Toast from '#/view/com/util/Toast'
011import {VIDEO_MAX_DURATION_MS} from '../constants'
12import {getDataUriSize} from './util'
13···53 quality: 1,
54 allowsMultipleSelection: true,
55 legacy: true,
56- base64: isWeb,
57- selectionLimit: isIOS ? selectionCountRemaining : undefined,
58 preferredAssetRepresentationMode:
59 UIImagePickerPreferredAssetRepresentationMode.Automatic,
60 videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
···5} from 'expo-image-picker'
6import {t} from '@lingui/macro'
708import {type ImageMeta} from '#/state/gallery'
9import * as Toast from '#/view/com/util/Toast'
10+import {IS_IOS, IS_WEB} from '#/env'
11import {VIDEO_MAX_DURATION_MS} from '../constants'
12import {getDataUriSize} from './util'
13···53 quality: 1,
54 allowsMultipleSelection: true,
55 legacy: true,
56+ base64: IS_WEB,
57+ selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
58 preferredAssetRepresentationMode:
59 UIImagePickerPreferredAssetRepresentationMode.Automatic,
60 videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
+2-2
src/lib/media/save-image.ios.ts
···2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
45-import {isNative} from '#/platform/detection'
6import * as Toast from '#/components/Toast'
07import {saveImageToMediaLibrary} from './manip'
89/**
···16 const {_} = useLingui()
17 return useCallback(
18 async (uri: string) => {
19- if (!isNative) {
20 throw new Error('useSaveImageToMediaLibrary is native only')
21 }
22
···2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
405import * as Toast from '#/components/Toast'
6+import {IS_NATIVE} from '#/env'
7import {saveImageToMediaLibrary} from './manip'
89/**
···16 const {_} = useLingui()
17 return useCallback(
18 async (uri: string) => {
19+ if (!IS_NATIVE) {
20 throw new Error('useSaveImageToMediaLibrary is native only')
21 }
22
+2-2
src/lib/media/save-image.ts
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isNative} from '#/platform/detection'
7import * as Toast from '#/components/Toast'
08import {saveImageToMediaLibrary} from './manip'
910/**
···18 })
19 return useCallback(
20 async (uri: string) => {
21- if (!isNative) {
22 throw new Error('useSaveImageToMediaLibrary is native only')
23 }
24
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import * as Toast from '#/components/Toast'
7+import {IS_NATIVE} from '#/env'
8import {saveImageToMediaLibrary} from './manip'
910/**
···18 })
19 return useCallback(
20 async (uri: string) => {
21+ if (!IS_NATIVE) {
22 throw new Error('useSaveImageToMediaLibrary is native only')
23 }
24
+5-5
src/lib/notifications/notifications.ts
···13} from '#/lib/constants'
14import {logger as notyLogger} from '#/lib/notifications/util'
15import {isNetworkError} from '#/lib/strings/errors'
16-import {isNative} from '#/platform/detection'
17import {type SessionAccount, useAgent, useSession} from '#/state/session'
18import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
19import {useAgeAssurance} from '#/ageAssurance'
020import {IS_DEV} from '#/env'
2122/**
···140 }: {
141 isAgeRestricted?: boolean
142 } = {}) => {
143- if (!isNative || IS_DEV) return
144145 /**
146 * This will also fire the listener added via `addPushTokenListener`. That
···236 const permissions = await Notifications.getPermissionsAsync()
237238 if (
239- !isNative ||
240 permissions?.status === 'granted' ||
241 (permissions?.status === 'denied' && !permissions.canAskAgain)
242 ) {
···277}
278279export async function decrementBadgeCount(by: number) {
280- if (!isNative) return
281282 let count = await getBadgeCountAsync()
283 count -= by
···295}
296297export async function unregisterPushToken(agents: AtpAgent[]) {
298- if (!isNative) return
299300 try {
301 const token = await getPushToken()
···13} from '#/lib/constants'
14import {logger as notyLogger} from '#/lib/notifications/util'
15import {isNetworkError} from '#/lib/strings/errors'
016import {type SessionAccount, useAgent, useSession} from '#/state/session'
17import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
18import {useAgeAssurance} from '#/ageAssurance'
19+import {IS_NATIVE} from '#/env'
20import {IS_DEV} from '#/env'
2122/**
···140 }: {
141 isAgeRestricted?: boolean
142 } = {}) => {
143+ if (!IS_NATIVE || IS_DEV) return
144145 /**
146 * This will also fire the listener added via `addPushTokenListener`. That
···236 const permissions = await Notifications.getPermissionsAsync()
237238 if (
239+ !IS_NATIVE ||
240 permissions?.status === 'granted' ||
241 (permissions?.status === 'denied' && !permissions.canAskAgain)
242 ) {
···277}
278279export async function decrementBadgeCount(by: number) {
280+ if (!IS_NATIVE) return
281282 let count = await getBadgeCountAsync()
283 count -= by
···295}
296297export async function unregisterPushToken(agents: AtpAgent[]) {
298+ if (!IS_NATIVE) return
299300 try {
301 const token = await getPushToken()
+3-3
src/lib/react-query.tsx
···9} from '@tanstack/react-query-persist-client'
10import type React from 'react'
1112-import {isNative, isWeb} from '#/platform/detection'
13import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
01415declare global {
16 interface Window {
···87}, 2000)
8889focusManager.setEventListener(onFocus => {
90- if (isNative) {
91 const subscription = AppState.addEventListener(
92 'change',
93 (status: AppStateStatus) => {
···187 }
188 })
189 useEffect(() => {
190- if (isWeb) {
191 window.__TANSTACK_QUERY_CLIENT__ = queryClient
192 }
193 }, [queryClient])
···9} from '@tanstack/react-query-persist-client'
10import type React from 'react'
11012import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
13+import {IS_NATIVE, IS_WEB} from '#/env'
1415declare global {
16 interface Window {
···87}, 2000)
8889focusManager.setEventListener(onFocus => {
90+ if (IS_NATIVE) {
91 const subscription = AppState.addEventListener(
92 'change',
93 (status: AppStateStatus) => {
···187 }
188 })
189 useEffect(() => {
190+ if (IS_WEB) {
191 window.__TANSTACK_QUERY_CLIENT__ = queryClient
192 }
193 }, [queryClient])
+4-4
src/lib/sharing.ts
···4// TODO: replace global i18n instance with one returned from useLingui -sfn
5import {t} from '@lingui/macro'
67-import {isAndroid, isIOS} from '#/platform/detection'
8import * as Toast from '#/view/com/util/Toast'
0910/**
11 * This function shares a URL using the native Share API if available, or copies it to the clipboard
···14 * clipboard.
15 */
16export async function shareUrl(url: string) {
17- if (isAndroid) {
18 await Share.share({message: url})
19- } else if (isIOS) {
20 await Share.share({url})
21 } else {
22 // React Native Share is not supported by web. Web Share API
···34 * clipboard.
35 */
36export async function shareText(text: string) {
37- if (isAndroid || isIOS) {
38 await Share.share({message: text})
39 } else {
40 await setStringAsync(text)
···4// TODO: replace global i18n instance with one returned from useLingui -sfn
5import {t} from '@lingui/macro'
607import * as Toast from '#/view/com/util/Toast'
8+import {IS_ANDROID, IS_IOS} from '#/env'
910/**
11 * This function shares a URL using the native Share API if available, or copies it to the clipboard
···14 * clipboard.
15 */
16export async function shareUrl(url: string) {
17+ if (IS_ANDROID) {
18 await Share.share({message: url})
19+ } else if (IS_IOS) {
20 await Share.share({url})
21 } else {
22 // React Native Share is not supported by web. Web Share API
···34 * clipboard.
35 */
36export async function shareText(text: string) {
37+ if (IS_ANDROID || IS_IOS) {
38 await Share.share({message: text})
39 } else {
40 await setStringAsync(text)
+2-2
src/lib/statsig/statsig.tsx
···56import {logger} from '#/logger'
7import {type MetricEvents} from '#/logger/metrics'
8-import {isWeb} from '#/platform/detection'
9import * as persisted from '#/state/persisted'
010import * as env from '#/env'
11import {useSession} from '../../state/session'
12import {timeout} from '../async/timeout'
···3738let refSrc = ''
39let refUrl = ''
40-if (isWeb && typeof window !== 'undefined') {
41 const params = new URLSearchParams(window.location.search)
42 refSrc = params.get('ref_src') ?? ''
43 refUrl = decodeURIComponent(params.get('ref_url') ?? '')
···56import {logger} from '#/logger'
7import {type MetricEvents} from '#/logger/metrics'
08import * as persisted from '#/state/persisted'
9+import {IS_WEB} from '#/env'
10import * as env from '#/env'
11import {useSession} from '../../state/session'
12import {timeout} from '../async/timeout'
···3738let refSrc = ''
39let refUrl = ''
40+if (IS_WEB && typeof window !== 'undefined') {
41 const params = new URLSearchParams(window.location.search)
42 refSrc = params.get('ref_src') ?? ''
43 refUrl = decodeURIComponent(params.get('ref_url') ?? '')
+4-4
src/lib/strings/embed-player.ts
···1import {Dimensions} from 'react-native'
23import {isSafari} from '#/lib/browser'
4-import {isWeb} from '#/platform/detection'
56const {height: SCREEN_HEIGHT} = Dimensions.get('window')
78-const IFRAME_HOST = isWeb
9 ? // @ts-ignore only for web
10 window.location.host === 'localhost:8100'
11 ? 'http://localhost:8100'
···132 urlp.hostname === 'www.twitch.tv' ||
133 urlp.hostname === 'm.twitch.tv'
134 ) {
135- const parent = isWeb
136 ? // @ts-ignore only for web
137 window.location.hostname
138 : 'localhost'
···559 width: Number(w),
560 }
561562- if (isWeb) {
563 if (isSafari) {
564 id = id.replace('AAAAC', 'AAAP1')
565 filename = filename.replace('.gif', '.mp4')
···1import {Dimensions} from 'react-native'
23import {isSafari} from '#/lib/browser'
4+import {IS_WEB} from '#/env'
56const {height: SCREEN_HEIGHT} = Dimensions.get('window')
78+const IFRAME_HOST = IS_WEB
9 ? // @ts-ignore only for web
10 window.location.host === 'localhost:8100'
11 ? 'http://localhost:8100'
···132 urlp.hostname === 'www.twitch.tv' ||
133 urlp.hostname === 'm.twitch.tv'
134 ) {
135+ const parent = IS_WEB
136 ? // @ts-ignore only for web
137 window.location.hostname
138 : 'localhost'
···559 width: Number(w),
560 }
561562+ if (IS_WEB) {
563 if (isSafari) {
564 id = id.replace('AAAAC', 'AAAP1')
565 filename = filename.replace('.gif', '.mp4')
+2-2
src/lib/styles.ts
···5 type TextStyle,
6} from 'react-native'
78-import {isWeb} from '#/platform/detection'
9import {type Theme, type TypographyVariant} from './ThemeContext'
1011// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
···186 // dimensions
187 w100pct: {width: '100%'},
188 h100pct: {height: '100%'},
189- hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'},
190 window: {
191 width: Dimensions.get('window').width,
192 height: Dimensions.get('window').height,
···5 type TextStyle,
6} from 'react-native'
78+import {IS_WEB} from '#/env'
9import {type Theme, type TypographyVariant} from './ThemeContext'
1011// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
···186 // dimensions
187 w100pct: {width: '100%'},
188 h100pct: {height: '100%'},
189+ hContentRegion: IS_WEB ? {minHeight: '100%'} : {height: '100%'},
190 window: {
191 width: Dimensions.get('window').width,
192 height: Dimensions.get('window').height,
+2-2
src/logger/index.ts
···13 type Transport,
14} from '#/logger/types'
15import {enabledLogLevels} from '#/logger/util'
16-import {isNative} from '#/platform/detection'
17import {ENV} from '#/env'
1819export {type MetricEvents as Metrics} from '#/logger/metrics'
···21const TRANSPORTS: Transport[] = (function configureTransports() {
22 switch (ENV) {
23 case 'production': {
24- return [sentryTransport, isNative && bitdriftTransport].filter(
25 Boolean,
26 ) as Transport[]
27 }
···13 type Transport,
14} from '#/logger/types'
15import {enabledLogLevels} from '#/logger/util'
16+import {IS_NATIVE} from '#/env'
17import {ENV} from '#/env'
1819export {type MetricEvents as Metrics} from '#/logger/metrics'
···21const TRANSPORTS: Transport[] = (function configureTransports() {
22 switch (ENV) {
23 case 'production': {
24+ return [sentryTransport, IS_NATIVE && bitdriftTransport].filter(
25 Boolean,
26 ) as Transport[]
27 }
+2-2
src/logger/transports/console.ts
···23import {LogLevel, type Transport} from '#/logger/types'
4import {prepareMetadata} from '#/logger/util'
5-import {isWeb} from '#/platform/detection'
67/**
8 * Used in dev mode to nicely log to the console
···33 msg += ` ${message.toString()}`
34 }
3536- if (isWeb) {
37 if (hasMetadata) {
38 console.groupCollapsed(msg)
39 console.log(metadata)
···23import {LogLevel, type Transport} from '#/logger/types'
4import {prepareMetadata} from '#/logger/util'
5+import {IS_WEB} from '#/env'
67/**
8 * Used in dev mode to nicely log to the console
···33 msg += ` ${message.toString()}`
34 }
3536+ if (IS_WEB) {
37 if (hasMetadata) {
38 console.groupCollapsed(msg)
39 console.log(metadata)
···21 type NativeStackScreenProps,
22} from '#/lib/routes/types'
23import {logger} from '#/logger'
24-import {isIOS} from '#/platform/detection'
25import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
26import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
27import {useSetMinimalShellMode} from '#/state/shell'
···38import * as Skele from '#/components/Skeleton'
39import * as toast from '#/components/Toast'
40import {Text} from '#/components/Typography'
04142type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
43···193 }
194 initialNumToRender={initialNumToRender}
195 windowSize={9}
196- maxToRenderPerBatch={isIOS ? 5 : 1}
197 updateCellsBatchingPeriod={40}
198 sideBorders={false}
199 />
···21 type NativeStackScreenProps,
22} from '#/lib/routes/types'
23import {logger} from '#/logger'
024import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
25import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
26import {useSetMinimalShellMode} from '#/state/shell'
···37import * as Skele from '#/components/Skeleton'
38import * as toast from '#/components/Toast'
39import {Text} from '#/components/Typography'
40+import {IS_IOS} from '#/env'
4142type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
43···193 }
194 initialNumToRender={initialNumToRender}
195 windowSize={9}
196+ maxToRenderPerBatch={IS_IOS ? 5 : 1}
197 updateCellsBatchingPeriod={40}
198 sideBorders={false}
199 />
+4-4
src/screens/Deactivated.tsx
···78import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
9import {logger} from '#/logger'
10-import {isWeb} from '#/platform/detection'
11import {
12 type SessionAccount,
13 useAgent,
···24import * as Layout from '#/components/Layout'
25import {Loader} from '#/components/Loader'
26import {Text} from '#/components/Typography'
02728const COL_WIDTH = 400
29···55 }, [setShowLoggedOut])
5657 const onPressLogout = React.useCallback(() => {
58- if (isWeb) {
59 // We're switching accounts, which remounts the entire app.
60 // On mobile, this gets us Home, but on the web we also need reset the URL.
61 // We can't change the URL via a navigate() call because the navigator
···101 contentContainerStyle={[
102 a.px_2xl,
103 {
104- paddingTop: isWeb ? 64 : insets.top + 16,
105- paddingBottom: isWeb ? 64 : insets.bottom,
106 },
107 ]}>
108 <View
···78import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
9import {logger} from '#/logger'
010import {
11 type SessionAccount,
12 useAgent,
···23import * as Layout from '#/components/Layout'
24import {Loader} from '#/components/Loader'
25import {Text} from '#/components/Typography'
26+import {IS_WEB} from '#/env'
2728const COL_WIDTH = 400
29···55 }, [setShowLoggedOut])
5657 const onPressLogout = React.useCallback(() => {
58+ if (IS_WEB) {
59 // We're switching accounts, which remounts the entire app.
60 // On mobile, this gets us Home, but on the web we also need reset the URL.
61 // We can't change the URL via a navigate() call because the navigator
···101 contentContainerStyle={[
102 a.px_2xl,
103 {
104+ paddingTop: IS_WEB ? 64 : insets.top + 16,
105+ paddingBottom: IS_WEB ? 64 : insets.bottom,
106 },
107 ]}>
108 <View
+2-2
src/screens/FindContactsFlowScreen.tsx
···9 type AllNavigatorParams,
10 type NativeStackScreenProps,
11} from '#/lib/routes/types'
12-import {isNative} from '#/platform/detection'
13import {useSetMinimalShellMode} from '#/state/shell'
14import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
15import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
16import {useFindContactsFlowState} from '#/components/contacts/state'
17import * as Layout from '#/components/Layout'
18import {ScreenTransition} from '#/components/ScreenTransition'
01920type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'>
21export function FindContactsFlowScreen({navigation}: Props) {
···4849 return (
50 <Layout.Screen>
51- {isNative ? (
52 <LayoutAnimationConfig skipEntering skipExiting>
53 <ScreenTransition key={state.step} direction={transitionDirection}>
54 <FindContactsFlow
···9 type AllNavigatorParams,
10 type NativeStackScreenProps,
11} from '#/lib/routes/types'
012import {useSetMinimalShellMode} from '#/state/shell'
13import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
14import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
15import {useFindContactsFlowState} from '#/components/contacts/state'
16import * as Layout from '#/components/Layout'
17import {ScreenTransition} from '#/components/ScreenTransition'
18+import {IS_NATIVE} from '#/env'
1920type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'>
21export function FindContactsFlowScreen({navigation}: Props) {
···4849 return (
50 <Layout.Screen>
51+ {IS_NATIVE ? (
52 <LayoutAnimationConfig skipEntering skipExiting>
53 <ScreenTransition key={state.step} direction={transitionDirection}>
54 <FindContactsFlow
+2-2
src/screens/Login/LoginForm.tsx
···18import {cleanError} from '#/lib/strings/errors'
19import {createFullHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
21-import {isIOS} from '#/platform/detection'
22import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
23import {useSessionApi} from '#/state/session'
24import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···32import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
33import {Loader} from '#/components/Loader'
34import {Text} from '#/components/Typography'
035import {FormContainer} from './FormContainer'
3637type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
···201 inputRef={identifierRef}
202 label={_(msg`Username or email address`)}
203 autoCapitalize="none"
204- autoFocus={!isIOS}
205 autoCorrect={false}
206 autoComplete="username"
207 returnKeyType="next"
···18import {cleanError} from '#/lib/strings/errors'
19import {createFullHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
021import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
22import {useSessionApi} from '#/state/session'
23import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···31import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
32import {Loader} from '#/components/Loader'
33import {Text} from '#/components/Typography'
34+import {IS_IOS} from '#/env'
35import {FormContainer} from './FormContainer'
3637type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
···201 inputRef={identifierRef}
202 label={_(msg`Username or email address`)}
203 autoCapitalize="none"
204+ autoFocus={!IS_IOS}
205 autoCorrect={false}
206 autoComplete="username"
207 returnKeyType="next"
+3-3
src/screens/Messages/ChatList.tsx
···13import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
14import {cleanError} from '#/lib/strings/errors'
15import {logger} from '#/logger'
16-import {isNative} from '#/platform/detection'
17import {listenSoftReset} from '#/state/events'
18import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
19import {useMessagesEventBus} from '#/state/messages/events'
···38import {Link} from '#/components/Link'
39import {ListFooter} from '#/components/Lists'
40import {Text} from '#/components/Typography'
041import {ChatListItem} from './components/ChatListItem'
42import {InboxPreview} from './components/InboxPreview'
43···222223 const onSoftReset = useCallback(async () => {
224 scrollElRef.current?.scrollToOffset({
225- animated: isNative,
226 offset: 0,
227 })
228 try {
···348 hasNextPage={hasNextPage}
349 />
350 }
351- onEndReachedThreshold={isNative ? 1.5 : 0}
352 initialNumToRender={initialNumToRender}
353 windowSize={11}
354 desktopFixedHeight
···13import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
14import {cleanError} from '#/lib/strings/errors'
15import {logger} from '#/logger'
016import {listenSoftReset} from '#/state/events'
17import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
18import {useMessagesEventBus} from '#/state/messages/events'
···37import {Link} from '#/components/Link'
38import {ListFooter} from '#/components/Lists'
39import {Text} from '#/components/Typography'
40+import {IS_NATIVE} from '#/env'
41import {ChatListItem} from './components/ChatListItem'
42import {InboxPreview} from './components/InboxPreview'
43···222223 const onSoftReset = useCallback(async () => {
224 scrollElRef.current?.scrollToOffset({
225+ animated: IS_NATIVE,
226 offset: 0,
227 })
228 try {
···348 hasNextPage={hasNextPage}
349 />
350 }
351+ onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
352 initialNumToRender={initialNumToRender}
353 windowSize={11}
354 desktopFixedHeight
+2-2
src/screens/Messages/Conversation.tsx
···21 type CommonNavigatorParams,
22 type NavigationProp,
23} from '#/lib/routes/types'
24-import {isWeb} from '#/platform/detection'
25import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
26import {useEmail} from '#/state/email-verification'
27import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
···43import {Error} from '#/components/Error'
44import * as Layout from '#/components/Layout'
45import {Loader} from '#/components/Loader'
04647type Props = NativeStackScreenProps<
48 CommonNavigatorParams,
···74 useCallback(() => {
75 setCurrentConvoId(convoId)
7677- if (isWeb && !gtMobile) {
78 setMinimalShellMode(true)
79 } else {
80 setMinimalShellMode(false)
···21 type CommonNavigatorParams,
22 type NavigationProp,
23} from '#/lib/routes/types'
024import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
25import {useEmail} from '#/state/email-verification'
26import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
···42import {Error} from '#/components/Error'
43import * as Layout from '#/components/Layout'
44import {Loader} from '#/components/Loader'
45+import {IS_WEB} from '#/env'
4647type Props = NativeStackScreenProps<
48 CommonNavigatorParams,
···74 useCallback(() => {
75 setCurrentConvoId(convoId)
7677+ if (IS_WEB && !gtMobile) {
78 setMinimalShellMode(true)
79 } else {
80 setMinimalShellMode(false)
+2-2
src/screens/Messages/Inbox.tsx
···21} from '#/lib/routes/types'
22import {cleanError} from '#/lib/strings/errors'
23import {logger} from '#/logger'
24-import {isNative} from '#/platform/detection'
25import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
26import {useMessagesEventBus} from '#/state/messages/events'
27import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
···44import * as Layout from '#/components/Layout'
45import {ListFooter} from '#/components/Lists'
46import {Text} from '#/components/Typography'
047import {RequestListItem} from './components/RequestListItem'
4849type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
···288 hasNextPage={hasNextPage}
289 />
290 }
291- onEndReachedThreshold={isNative ? 1.5 : 0}
292 initialNumToRender={initialNumToRender}
293 windowSize={11}
294 desktopFixedHeight
···21} from '#/lib/routes/types'
22import {cleanError} from '#/lib/strings/errors'
23import {logger} from '#/logger'
024import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
25import {useMessagesEventBus} from '#/state/messages/events'
26import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
···43import * as Layout from '#/components/Layout'
44import {ListFooter} from '#/components/Lists'
45import {Text} from '#/components/Typography'
46+import {IS_NATIVE} from '#/env'
47import {RequestListItem} from './components/RequestListItem'
4849type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
···288 hasNextPage={hasNextPage}
289 />
290 }
291+ onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
292 initialNumToRender={initialNumToRender}
293 windowSize={11}
294 desktopFixedHeight
+2-2
src/screens/Messages/Settings.tsx
···5import {type NativeStackScreenProps} from '@react-navigation/native-stack'
67import {type CommonNavigatorParams} from '#/lib/routes/types'
8-import {isNative} from '#/platform/detection'
9import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
10import {useProfileQuery} from '#/state/queries/profile'
11import {useSession} from '#/state/session'
···16import * as Toggle from '#/components/forms/Toggle'
17import * as Layout from '#/components/Layout'
18import {Text} from '#/components/Typography'
019import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
2021type AllowIncoming = 'all' | 'none' | 'following'
···118 you choose.
119 </Trans>
120 </Admonition>
121- {isNative && (
122 <>
123 <Divider style={a.my_md} />
124 <Text style={[a.text_lg, a.font_semi_bold]}>
···5import {type NativeStackScreenProps} from '@react-navigation/native-stack'
67import {type CommonNavigatorParams} from '#/lib/routes/types'
08import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
9import {useProfileQuery} from '#/state/queries/profile'
10import {useSession} from '#/state/session'
···15import * as Toggle from '#/components/forms/Toggle'
16import * as Layout from '#/components/Layout'
17import {Text} from '#/components/Typography'
18+import {IS_NATIVE} from '#/env'
19import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
2021type AllowIncoming = 'all' | 'none' | 'following'
···118 you choose.
119 </Trans>
120 </Admonition>
121+ {IS_NATIVE && (
122 <>
123 <Divider style={a.my_md} />
124 <Text style={[a.text_lg, a.font_semi_bold]}>
+4-4
src/screens/Messages/components/ChatListItem.tsx
···20 toBskyAppUrl,
21 toShortUrl,
22} from '#/lib/strings/url-helpers'
23-import {isNative} from '#/platform/detection'
24import {useProfileShadow} from '#/state/cache/profile-shadow'
25import {useModerationOpts} from '#/state/preferences/moderation-opts'
26import {
···46import {Text} from '#/components/Typography'
47import {useSimpleVerificationState} from '#/components/verification'
48import {VerificationCheck} from '#/components/verification/VerificationCheck'
049import type * as bsky from '#/types/bsky'
5051export const ChatListItemPortal = createPortalGroup()
···366 )
367 }
368 accessibilityActions={
369- isNative
370 ? [
371 {
372 name: 'magicTap',
···380 : undefined
381 }
382 onPress={onPress}
383- onLongPress={isNative ? onLongPress : undefined}
384 onAccessibilityAction={onLongPress}>
385 {({hovered, pressed, focused}) => (
386 <View
···519 control={menuControl}
520 currentScreen="list"
521 showMarkAsRead={convo.unreadCount > 0}
522- hideTrigger={isNative}
523 blockInfo={blockInfo}
524 style={[
525 a.absolute,
···20 toBskyAppUrl,
21 toShortUrl,
22} from '#/lib/strings/url-helpers'
023import {useProfileShadow} from '#/state/cache/profile-shadow'
24import {useModerationOpts} from '#/state/preferences/moderation-opts'
25import {
···45import {Text} from '#/components/Typography'
46import {useSimpleVerificationState} from '#/components/verification'
47import {VerificationCheck} from '#/components/verification/VerificationCheck'
48+import {IS_NATIVE} from '#/env'
49import type * as bsky from '#/types/bsky'
5051export const ChatListItemPortal = createPortalGroup()
···366 )
367 }
368 accessibilityActions={
369+ IS_NATIVE
370 ? [
371 {
372 name: 'magicTap',
···380 : undefined
381 }
382 onPress={onPress}
383+ onLongPress={IS_NATIVE ? onLongPress : undefined}
384 onAccessibilityAction={onLongPress}>
385 {({hovered, pressed, focused}) => (
386 <View
···519 control={menuControl}
520 currentScreen="list"
521 showMarkAsRead={convo.unreadCount > 0}
522+ hideTrigger={IS_NATIVE}
523 blockInfo={blockInfo}
524 style={[
525 a.absolute,
+5-5
src/screens/Messages/components/MessageInput.tsx
···1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
21-import {isIOS, isWeb} from '#/platform/detection'
22import {useEmail} from '#/state/email-verification'
23import {
24 useMessageDraft,
···29import {android, atoms as a, useTheme} from '#/alf'
30import {useSharedInputStyles} from '#/components/forms/TextField'
31import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
032import {useExtractEmbedFromFacets} from './MessageInputEmbed'
3334const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
···84 playHaptic()
85 setEmbed(undefined)
86 setMessage('')
87- if (isIOS) {
88 setShouldEnforceClear(true)
89 }
90- if (isWeb) {
91 // Pressing the send button causes the text input to lose focus, so we need to
92 // re-focus it after sending
93 setTimeout(() => {
···160 // next change and double make sure the input is cleared. It should *always* send an onChange event after
161 // clearing via setMessage('') that happens in onSubmit()
162 // -sfn
163- if (isIOS && shouldEnforceClear) {
164 setShouldEnforceClear(false)
165 setMessage('')
166 return
···175 a.px_sm,
176 t.atoms.text,
177 android({paddingTop: 0}),
178- {paddingBottom: isIOS ? 5 : 0},
179 animatedStyle,
180 ]}
181 keyboardAppearance={t.scheme}
···1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
021import {useEmail} from '#/state/email-verification'
22import {
23 useMessageDraft,
···28import {android, atoms as a, useTheme} from '#/alf'
29import {useSharedInputStyles} from '#/components/forms/TextField'
30import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
31+import {IS_IOS, IS_WEB} from '#/env'
32import {useExtractEmbedFromFacets} from './MessageInputEmbed'
3334const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
···84 playHaptic()
85 setEmbed(undefined)
86 setMessage('')
87+ if (IS_IOS) {
88 setShouldEnforceClear(true)
89 }
90+ if (IS_WEB) {
91 // Pressing the send button causes the text input to lose focus, so we need to
92 // re-focus it after sending
93 setTimeout(() => {
···160 // next change and double make sure the input is cleared. It should *always* send an onChange event after
161 // clearing via setMessage('') that happens in onSubmit()
162 // -sfn
163+ if (IS_IOS && shouldEnforceClear) {
164 setShouldEnforceClear(false)
165 setMessage('')
166 return
···175 a.px_sm,
176 t.atoms.text,
177 android({paddingTop: 0}),
178+ {paddingBottom: IS_IOS ? 5 : 0},
179 animatedStyle,
180 ]}
181 keyboardAppearance={t.scheme}
+7-7
src/screens/Messages/components/MessagesList.tsx
···24 isBskyPostUrl,
25} from '#/lib/strings/url-helpers'
26import {logger} from '#/logger'
27-import {isNative} from '#/platform/detection'
28-import {isWeb} from '#/platform/detection'
29import {
30 type ActiveConvoStates,
31 isConvoActive,
···52import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
53import {Loader} from '#/components/Loader'
54import {Text} from '#/components/Typography'
0055import {ChatStatusInfo} from './ChatStatusInfo'
56import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
57···159 (_: number, height: number) => {
160 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
161 // previous off whenever we add new content to the previous offset whenever we add new content to the list.
162- if (isWeb && isAtTop.get() && hasScrolled) {
163 flatListRef.current?.scrollToOffset({
164 offset: height - prevContentHeight.current,
165 animated: false,
···390 (e: LayoutChangeEvent) => {
391 layoutHeight.set(e.nativeEvent.layout.height)
392393- if (isWeb || !keyboardIsOpening.get()) {
394 flatListRef.current?.scrollToEnd({
395 animated: !layoutScrollWithoutAnimation.get(),
396 })
···429 disableVirtualization={true}
430 style={animatedListStyle}
431 // The extra two items account for the header and the footer components
432- initialNumToRender={isNative ? 32 : 62}
433- maxToRenderPerBatch={isWeb ? 32 : 62}
434 keyboardDismissMode="on-drag"
435 keyboardShouldPersistTaps="handled"
436 maintainVisibleContentPosition={{
···468 )}
469 </Animated.View>
470471- {isWeb && (
472 <EmojiPicker
473 pinToTop
474 state={emojiPickerState}
···24 isBskyPostUrl,
25} from '#/lib/strings/url-helpers'
26import {logger} from '#/logger'
0027import {
28 type ActiveConvoStates,
29 isConvoActive,
···50import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
51import {Loader} from '#/components/Loader'
52import {Text} from '#/components/Typography'
53+import {IS_NATIVE} from '#/env'
54+import {IS_WEB} from '#/env'
55import {ChatStatusInfo} from './ChatStatusInfo'
56import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
57···159 (_: number, height: number) => {
160 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
161 // previous off whenever we add new content to the previous offset whenever we add new content to the list.
162+ if (IS_WEB && isAtTop.get() && hasScrolled) {
163 flatListRef.current?.scrollToOffset({
164 offset: height - prevContentHeight.current,
165 animated: false,
···390 (e: LayoutChangeEvent) => {
391 layoutHeight.set(e.nativeEvent.layout.height)
392393+ if (IS_WEB || !keyboardIsOpening.get()) {
394 flatListRef.current?.scrollToEnd({
395 animated: !layoutScrollWithoutAnimation.get(),
396 })
···429 disableVirtualization={true}
430 style={animatedListStyle}
431 // The extra two items account for the header and the footer components
432+ initialNumToRender={IS_NATIVE ? 32 : 62}
433+ maxToRenderPerBatch={IS_WEB ? 32 : 62}
434 keyboardDismissMode="on-drag"
435 keyboardShouldPersistTaps="handled"
436 maintainVisibleContentPosition={{
···468 )}
469 </Animated.View>
470471+ {IS_WEB && (
472 <EmojiPicker
473 pinToTop
474 state={emojiPickerState}
+2-2
src/screens/Moderation/index.tsx
···11 type NativeStackScreenProps,
12} from '#/lib/routes/types'
13import {logger} from '#/logger'
14-import {isIOS} from '#/platform/detection'
15import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
16import {
17 useMyLabelersQuery,
···45import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
46import {Text} from '#/components/Typography'
47import {useAgeAssurance} from '#/ageAssurance'
04849function ErrorState({error}: {error: string}) {
50 const t = useTheme()
···182 (optimisticAdultContent && optimisticAdultContent.enabled) ||
183 (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
184 )
185- const adultContentUIDisabledOnIOS = isIOS && !adultContentEnabled
186 let adultContentUIDisabled = adultContentUIDisabledOnIOS
187188 if (aa.flags.adultContentDisabled) {
···11 type NativeStackScreenProps,
12} from '#/lib/routes/types'
13import {logger} from '#/logger'
014import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
15import {
16 useMyLabelersQuery,
···44import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
45import {Text} from '#/components/Typography'
46import {useAgeAssurance} from '#/ageAssurance'
47+import {IS_IOS} from '#/env'
4849function ErrorState({error}: {error: string}) {
50 const t = useTheme()
···182 (optimisticAdultContent && optimisticAdultContent.enabled) ||
183 (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
184 )
185+ const adultContentUIDisabledOnIOS = IS_IOS && !adultContentEnabled
186 let adultContentUIDisabled = adultContentUIDisabledOnIOS
187188 if (aa.flags.adultContentDisabled) {
···22import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
23import {logEvent, useGate} from '#/lib/statsig/statsig'
24import {logger} from '#/logger'
25-import {isWeb} from '#/platform/detection'
26import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
27import {getAllListMembers} from '#/state/queries/list-members'
28import {preferencesQueryKey} from '#/state/queries/preferences'
···47import {Button, ButtonIcon, ButtonText} from '#/components/Button'
48import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
49import {Loader} from '#/components/Loader'
050import * as bsky from '#/types/bsky'
51import {ValuePropositionPager} from './ValuePropositionPager'
52···305306 <OnboardingControls.Portal>
307 <View style={gtMobile && [a.gap_md, a.flex_row]}>
308- {gtMobile && (isWeb ? subStep !== 2 : true) && (
309 <Button
310 disabled={saving}
311 color="secondary"
···22import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
23import {logEvent, useGate} from '#/lib/statsig/statsig'
24import {logger} from '#/logger'
025import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
26import {getAllListMembers} from '#/state/queries/list-members'
27import {preferencesQueryKey} from '#/state/queries/preferences'
···46import {Button, ButtonIcon, ButtonText} from '#/components/Button'
47import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
48import {Loader} from '#/components/Loader'
49+import {IS_WEB} from '#/env'
50import * as bsky from '#/types/bsky'
51import {ValuePropositionPager} from './ValuePropositionPager'
52···305306 <OnboardingControls.Portal>
307 <View style={gtMobile && [a.gap_md, a.flex_row]}>
308+ {gtMobile && (IS_WEB ? subStep !== 2 : true) && (
309 <Button
310 disabled={saving}
311 color="secondary"
+3-3
src/screens/Onboarding/StepProfile/index.tsx
···17import {logEvent, useGate} from '#/lib/statsig/statsig'
18import {isCancelledError} from '#/lib/strings/errors'
19import {logger} from '#/logger'
20-import {isNative, isWeb} from '#/platform/detection'
21import {
22 OnboardingControls,
23 OnboardingDescriptionText,
···38import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
39import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
40import {Text} from '#/components/Typography'
041import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
4243export interface Avatar {
···183 let image = items[0]
184 if (!image) return
185186- if (!isWeb) {
187 try {
188 image = await openCropper({
189 imageUri: image.path,
···200201 // If we are on mobile, prefetching the image will load the image into memory before we try and display it,
202 // stopping any brief flickers.
203- if (isNative) {
204 await ExpoImage.prefetch(image.path)
205 }
206
···17import {logEvent, useGate} from '#/lib/statsig/statsig'
18import {isCancelledError} from '#/lib/strings/errors'
19import {logger} from '#/logger'
020import {
21 OnboardingControls,
22 OnboardingDescriptionText,
···37import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
38import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
39import {Text} from '#/components/Typography'
40+import {IS_NATIVE, IS_WEB} from '#/env'
41import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
4243export interface Avatar {
···183 let image = items[0]
184 if (!image) return
185186+ if (!IS_WEB) {
187 try {
188 image = await openCropper({
189 imageUri: image.path,
···200201 // If we are on mobile, prefetching the image will load the image into memory before we try and display it,
202 // stopping any brief flickers.
203+ if (IS_NATIVE) {
204 await ExpoImage.prefetch(image.path)
205 }
206
···10import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
11import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
12import {logger} from '#/logger'
13-import {isWeb} from '#/platform/detection'
14import {updateProfileShadow} from '#/state/cache/profile-shadow'
15import {useLanguagePrefs} from '#/state/preferences'
16import {useModerationOpts} from '#/state/preferences/moderation-opts'
···31import {Loader} from '#/components/Loader'
32import * as ProfileCard from '#/components/ProfileCard'
33import * as toast from '#/components/Toast'
034import type * as bsky from '#/types/bsky'
35import {bulkWriteFollows} from '../util'
36···161 style={[
162 a.overflow_hidden,
163 a.mt_sm,
164- isWeb
165 ? [a.max_w_full, web({minHeight: '100vh'})]
166 : {marginHorizontal: tokens.space.xl * -1},
167 a.flex_1,
···213 a.mt_md,
214 a.border_y,
215 t.atoms.border_contrast_low,
216- isWeb && [a.border_x, a.rounded_sm, a.overflow_hidden],
217 ]}>
218 {suggestedUsers?.actors.map((user, index) => (
219 <SuggestedProfileCard
···324 ...interestsDisplayNames,
325 }
326 }
327- gutterWidth={isWeb ? 0 : tokens.space.xl}
328 />
329 )
330}
···350 const node = cardRef.current
351 if (!node || hasTrackedRef.current) return
352353- if (isWeb && typeof IntersectionObserver !== 'undefined') {
354 const observer = new IntersectionObserver(
355 entries => {
356 if (entries[0]?.isIntersecting && !hasTrackedRef.current) {
···10import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
11import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
12import {logger} from '#/logger'
013import {updateProfileShadow} from '#/state/cache/profile-shadow'
14import {useLanguagePrefs} from '#/state/preferences'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
···30import {Loader} from '#/components/Loader'
31import * as ProfileCard from '#/components/ProfileCard'
32import * as toast from '#/components/Toast'
33+import {IS_WEB} from '#/env'
34import type * as bsky from '#/types/bsky'
35import {bulkWriteFollows} from '../util'
36···161 style={[
162 a.overflow_hidden,
163 a.mt_sm,
164+ IS_WEB
165 ? [a.max_w_full, web({minHeight: '100vh'})]
166 : {marginHorizontal: tokens.space.xl * -1},
167 a.flex_1,
···213 a.mt_md,
214 a.border_y,
215 t.atoms.border_contrast_low,
216+ IS_WEB && [a.border_x, a.rounded_sm, a.overflow_hidden],
217 ]}>
218 {suggestedUsers?.actors.map((user, index) => (
219 <SuggestedProfileCard
···324 ...interestsDisplayNames,
325 }
326 }
327+ gutterWidth={IS_WEB ? 0 : tokens.space.xl}
328 />
329 )
330}
···350 const node = cardRef.current
351 if (!node || hasTrackedRef.current) return
352353+ if (IS_WEB && typeof IntersectionObserver !== 'undefined') {
354 const observer = new IntersectionObserver(
355 entries => {
356 if (entries[0]?.isIntersecting && !hasTrackedRef.current) {
+2-2
src/screens/Onboarding/index.tsx
···45import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
6import {useGate} from '#/lib/statsig/statsig'
7-import {isNative} from '#/platform/detection'
8import {useLanguagePrefs} from '#/state/preferences'
9import {
10 Layout,
···24import {useFindContactsFlowState} from '#/components/contacts/state'
25import {Portal} from '#/components/Portal'
26import {ScreenTransition} from '#/components/ScreenTransition'
027import {ENV} from '#/env'
28import {StepFindContacts} from './StepFindContacts'
29import {StepFindContactsIntro} from './StepFindContactsIntro'
···50 useIsFindContactsFeatureEnabledBasedOnGeolocation()
51 const showFindContacts =
52 ENV !== 'e2e' &&
53- isNative &&
54 findContactsEnabled &&
55 !gate('disable_onboarding_find_contacts')
56
···45import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
6import {useGate} from '#/lib/statsig/statsig'
07import {useLanguagePrefs} from '#/state/preferences'
8import {
9 Layout,
···23import {useFindContactsFlowState} from '#/components/contacts/state'
24import {Portal} from '#/components/Portal'
25import {ScreenTransition} from '#/components/ScreenTransition'
26+import {IS_NATIVE} from '#/env'
27import {ENV} from '#/env'
28import {StepFindContacts} from './StepFindContacts'
29import {StepFindContactsIntro} from './StepFindContactsIntro'
···50 useIsFindContactsFeatureEnabledBasedOnGeolocation()
51 const showFindContacts =
52 ENV !== 'e2e' &&
53+ IS_NATIVE &&
54 findContactsEnabled &&
55 !gate('disable_onboarding_find_contacts')
56
+2-2
src/screens/PostThread/components/GrowthHack.tsx
···3import {PrivacySensitive} from 'expo-privacy-sensitive'
45import {useAppState} from '#/lib/hooks/useAppState'
6-import {isIOS} from '#/platform/detection'
7import {atoms as a, useTheme} from '#/alf'
8import {sizes as iconSizes} from '#/components/icons/common'
9import {Mark as Logo} from '#/components/icons/Logo'
01011const ICON_SIZE = 'xl' as const
12···2526 const appState = useAppState()
2728- if (!isIOS || appState !== 'active') return children
2930 return (
31 <View
···3import {PrivacySensitive} from 'expo-privacy-sensitive'
45import {useAppState} from '#/lib/hooks/useAppState'
06import {atoms as a, useTheme} from '#/alf'
7import {sizes as iconSizes} from '#/components/icons/common'
8import {Mark as Logo} from '#/components/icons/Logo'
9+import {IS_IOS} from '#/env'
1011const ICON_SIZE = 'xl' as const
12···2526 const appState = useAppState()
2728+ if (!IS_IOS || appState !== 'active') return children
2930 return (
31 <View
···5import {useNavigation} from '@react-navigation/native'
67import {logger} from '#/logger'
8-import {isIOS} from '#/platform/detection'
9import {useProfileShadow} from '#/state/cache/profile-shadow'
10import {
11 useProfileFollowMutationQueue,
···17import {Button, ButtonIcon, ButtonText} from '#/components/Button'
18import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
19import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
020import {GrowthHack} from './GrowthHack'
2122export function ThreadItemAnchorFollowButton({did}: {did: string}) {
23- if (isIOS) {
24 return (
25 <GrowthHack>
26 <ThreadItemAnchorFollowButtonInner did={did} />
···5import {useNavigation} from '@react-navigation/native'
67import {logger} from '#/logger'
08import {useProfileShadow} from '#/state/cache/profile-shadow'
9import {
10 useProfileFollowMutationQueue,
···16import {Button, ButtonIcon, ButtonText} from '#/components/Button'
17import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
18import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
19+import {IS_IOS} from '#/env'
20import {GrowthHack} from './GrowthHack'
2122export function ThreadItemAnchorFollowButton({did}: {did: string}) {
23+ if (IS_IOS) {
24 return (
25 <GrowthHack>
26 <ThreadItemAnchorFollowButtonInner did={did} />
+2-2
src/screens/Profile/Header/GrowableAvatar.tsx
···7} from 'react-native-reanimated'
8import type React from 'react'
910-import {isIOS} from '#/platform/detection'
11import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
01213export function GrowableAvatar({
14 children,
···20 const pagerContext = usePagerHeaderContext()
2122 // pagerContext should only be present on iOS, but better safe than sorry
23- if (!pagerContext || !isIOS) {
24 return <View style={style}>{children}</View>
25 }
26
···7} from 'react-native-reanimated'
8import type React from 'react'
9010import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
11+import {IS_IOS} from '#/env'
1213export function GrowableAvatar({
14 children,
···20 const pagerContext = usePagerHeaderContext()
2122 // pagerContext should only be present on iOS, but better safe than sorry
23+ if (!pagerContext || !IS_IOS) {
24 return <View style={style}>{children}</View>
25 }
26
+3-3
src/screens/Profile/Header/GrowableBanner.tsx
···15import {useIsFetching} from '@tanstack/react-query'
16import type React from 'react'
1718-import {isIOS} from '#/platform/detection'
19import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
20import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
21import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
22import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
23import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
24import {atoms as a} from '#/alf'
02526const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
27···39 const pagerContext = usePagerHeaderContext()
4041 // plain non-growable mode for Android/Web
42- if (!pagerContext || !isIOS) {
43 return (
44 <Pressable
45 onPress={onPress}
···164 style={[
165 a.absolute,
166 a.inset_0,
167- {top: topInset - (isIOS ? 15 : 0)},
168 a.justify_center,
169 a.align_center,
170 ]}>
···15import {useIsFetching} from '@tanstack/react-query'
16import type React from 'react'
17018import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
19import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
20import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
21import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
22import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
23import {atoms as a} from '#/alf'
24+import {IS_IOS} from '#/env'
2526const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
27···39 const pagerContext = usePagerHeaderContext()
4041 // plain non-growable mode for Android/Web
42+ if (!pagerContext || !IS_IOS) {
43 return (
44 <Pressable
45 onPress={onPress}
···164 style={[
165 a.absolute,
166 a.inset_0,
167+ {top: topInset - (IS_IOS ? 15 : 0)},
168 a.justify_center,
169 a.align_center,
170 ]}>
+3-3
src/screens/Profile/Header/Handle.tsx
···4import {useLingui} from '@lingui/react'
56import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
7-import {isIOS, isNative} from '#/platform/detection'
8import {type Shadow} from '#/state/cache/types'
9import {atoms as a, useTheme, web} from '#/alf'
10import {NewskieDialog} from '#/components/NewskieDialog'
11import {Text} from '#/components/Typography'
01213export function ProfileHeaderHandle({
14 profile,
···24 return (
25 <View
26 style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]}
27- pointerEvents={disableTaps ? 'none' : isIOS ? 'auto' : 'box-none'}>
28 <NewskieDialog profile={profile} disabled={disableTaps} />
29 {profile.viewer?.followedBy && !blockHide ? (
30 <View style={[t.atoms.bg_contrast_50, a.rounded_xs, a.px_sm, a.py_xs]}>
···59 profile.handle,
60 '@',
61 // forceLTR handled by CSS above on web
62- isNative,
63 )}
64 </Text>
65 </View>
···4import {useLingui} from '@lingui/react'
56import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
07import {type Shadow} from '#/state/cache/types'
8import {atoms as a, useTheme, web} from '#/alf'
9import {NewskieDialog} from '#/components/NewskieDialog'
10import {Text} from '#/components/Typography'
11+import {IS_IOS, IS_NATIVE} from '#/env'
1213export function ProfileHeaderHandle({
14 profile,
···24 return (
25 <View
26 style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]}
27+ pointerEvents={disableTaps ? 'none' : IS_IOS ? 'auto' : 'box-none'}>
28 <NewskieDialog profile={profile} disabled={disableTaps} />
29 {profile.viewer?.followedBy && !blockHide ? (
30 <View style={[t.atoms.bg_contrast_50, a.rounded_xs, a.px_sm, a.py_xs]}>
···59 profile.handle,
60 '@',
61 // forceLTR handled by CSS above on web
62+ IS_NATIVE,
63 )}
64 </Text>
65 </View>
···15import {sanitizeDisplayName} from '#/lib/strings/display-names'
16import {sanitizeHandle} from '#/lib/strings/handles'
17import {logger} from '#/logger'
18-import {isIOS} from '#/platform/detection'
19import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
20import {
21 useProfileBlockMutationQueue,
···39import * as Toast from '#/components/Toast'
40import {Text} from '#/components/Typography'
41import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
042import {EditProfileDialog} from './EditProfileDialog'
43import {ProfileHeaderHandle} from './Handle'
44import {ProfileHeaderMetrics} from './Metrics'
···103 isPlaceholderProfile={isPlaceholderProfile}>
104 <View
105 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
106- pointerEvents={isIOS ? 'auto' : 'box-none'}>
107 <View
108 style={[
109 {paddingLeft: 90},
···114 a.pb_sm,
115 a.flex_wrap,
116 ]}
117- pointerEvents={isIOS ? 'auto' : 'box-none'}>
118 <HeaderStandardButtons
119 profile={profile}
120 moderation={moderation}
···15import {sanitizeDisplayName} from '#/lib/strings/display-names'
16import {sanitizeHandle} from '#/lib/strings/handles'
17import {logger} from '#/logger'
018import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
19import {
20 useProfileBlockMutationQueue,
···38import * as Toast from '#/components/Toast'
39import {Text} from '#/components/Typography'
40import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
41+import {IS_IOS} from '#/env'
42import {EditProfileDialog} from './EditProfileDialog'
43import {ProfileHeaderHandle} from './Handle'
44import {ProfileHeaderMetrics} from './Metrics'
···103 isPlaceholderProfile={isPlaceholderProfile}>
104 <View
105 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
106+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
107 <View
108 style={[
109 {paddingLeft: 90},
···114 a.pb_sm,
115 a.flex_wrap,
116 ]}
117+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
118 <HeaderStandardButtons
119 profile={profile}
120 moderation={moderation}
+5-5
src/screens/Profile/Header/Shell.tsx
···19import {useHaptics} from '#/lib/haptics'
20import {type NavigationProp} from '#/lib/routes/types'
21import {logger} from '#/logger'
22-import {isIOS} from '#/platform/detection'
23import {type Shadow} from '#/state/cache/types'
24import {useLightboxControls} from '#/state/lightbox'
25import {useSession} from '#/state/session'
···35import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
36import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
37import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
038import {GrowableAvatar} from './GrowableAvatar'
39import {GrowableBanner} from './GrowableBanner'
40import {StatusBarShadow} from './StatusBarShadow'
···167 }, [profile.banner, moderation, _openLightbox, bannerRef])
168169 return (
170- <View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
171 <View
172- pointerEvents={isIOS ? 'auto' : 'box-none'}
173 style={[a.relative, {height: 150}]}>
174 <StatusBarShadow />
175 <GrowableBanner
···244 a.px_lg,
245 a.pt_xs,
246 a.pb_sm,
247- isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
248 ]}
249 />
250 ) : (
···254 a.px_lg,
255 a.pt_xs,
256 a.pb_sm,
257- isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
258 ]}
259 />
260 ))}
···19import {useHaptics} from '#/lib/haptics'
20import {type NavigationProp} from '#/lib/routes/types'
21import {logger} from '#/logger'
022import {type Shadow} from '#/state/cache/types'
23import {useLightboxControls} from '#/state/lightbox'
24import {useSession} from '#/state/session'
···34import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
35import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
36import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
37+import {IS_IOS} from '#/env'
38import {GrowableAvatar} from './GrowableAvatar'
39import {GrowableBanner} from './GrowableBanner'
40import {StatusBarShadow} from './StatusBarShadow'
···167 }, [profile.banner, moderation, _openLightbox, bannerRef])
168169 return (
170+ <View style={t.atoms.bg} pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
171 <View
172+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
173 style={[a.relative, {height: 150}]}>
174 <StatusBarShadow />
175 <GrowableBanner
···244 a.px_lg,
245 a.pt_xs,
246 a.pb_sm,
247+ IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
248 ]}
249 />
250 ) : (
···254 a.px_lg,
255 a.pt_xs,
256 a.pb_sm,
257+ IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
258 ]}
259 />
260 ))}
+2-2
src/screens/Profile/Header/StatusBarShadow.tsx
···5import {useSafeAreaInsets} from 'react-native-safe-area-context'
6import {LinearGradient} from 'expo-linear-gradient'
78-import {isIOS} from '#/platform/detection'
9import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
10import {atoms as a} from '#/alf'
01112const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
13···15 const {top: topInset} = useSafeAreaInsets()
16 const pagerContext = usePagerHeaderContext()
1718- if (isIOS && pagerContext) {
19 const {scrollY} = pagerContext
20 return <StatusBarShadowInnner scrollY={scrollY} />
21 }
···5import {useSafeAreaInsets} from 'react-native-safe-area-context'
6import {LinearGradient} from 'expo-linear-gradient'
708import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
9import {atoms as a} from '#/alf'
10+import {IS_IOS} from '#/env'
1112const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
13···15 const {top: topInset} = useSafeAreaInsets()
16 const pagerContext = usePagerHeaderContext()
1718+ if (IS_IOS && pagerContext) {
19 const {scrollY} = pagerContext
20 return <StatusBarShadowInnner scrollY={scrollY} />
21 }
+2-2
src/screens/Profile/Header/SuggestedFollows.tsx
···2import {type AppBskyActorDefs} from '@atproto/api'
34import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
5-import {isAndroid} from '#/platform/detection'
6import {useModerationOpts} from '#/state/preferences/moderation-opts'
7import {
8 useSuggestedFollowsByActorQuery,
···10} from '#/state/queries/suggested-follows'
11import {useBreakpoints} from '#/alf'
12import {ProfileGrid} from '#/components/FeedInterstitials'
01314const DISMISS_ANIMATION_DURATION = 200
15···210 * This issue stems from Android not allowing dragging on clickable elements in the profile header.
211 * Blocking the ability to scroll on Android is too much of a trade-off for now.
212 **/
213- if (isAndroid) return null
214215 return (
216 <AccordionAnimation isExpanded={isExpanded}>
···2import {type AppBskyActorDefs} from '@atproto/api'
34import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
05import {useModerationOpts} from '#/state/preferences/moderation-opts'
6import {
7 useSuggestedFollowsByActorQuery,
···9} from '#/state/queries/suggested-follows'
10import {useBreakpoints} from '#/alf'
11import {ProfileGrid} from '#/components/FeedInterstitials'
12+import {IS_ANDROID} from '#/env'
1314const DISMISS_ANIMATION_DURATION = 200
15···210 * This issue stems from Android not allowing dragging on clickable elements in the profile header.
211 * Blocking the ability to scroll on Android is too much of a trade-off for now.
212 **/
213+ if (IS_ANDROID) return null
214215 return (
216 <AccordionAnimation isExpanded={isExpanded}>
+2-2
src/screens/Profile/Header/index.tsx
···17import {useIsFocused} from '@react-navigation/native'
1819import {sanitizeHandle} from '#/lib/strings/handles'
20-import {isNative} from '#/platform/detection'
21import {useProfileShadow} from '#/state/cache/profile-shadow'
22import {useModerationOpts} from '#/state/preferences/moderation-opts'
23import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
···26import {atoms as a, useTheme} from '#/alf'
27import {Header} from '#/components/Layout'
28import * as ProfileCard from '#/components/ProfileCard'
029import {
30 HeaderLabelerButtons,
31 ProfileHeaderLabeler,
···8384 return (
85 <>
86- {isNative && (
87 <MinimalHeader
88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
89 profile={props.profile}
···17import {useIsFocused} from '@react-navigation/native'
1819import {sanitizeHandle} from '#/lib/strings/handles'
020import {useProfileShadow} from '#/state/cache/profile-shadow'
21import {useModerationOpts} from '#/state/preferences/moderation-opts'
22import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
···25import {atoms as a, useTheme} from '#/alf'
26import {Header} from '#/components/Layout'
27import * as ProfileCard from '#/components/ProfileCard'
28+import {IS_NATIVE} from '#/env'
29import {
30 HeaderLabelerButtons,
31 ProfileHeaderLabeler,
···8384 return (
85 <>
86+ {IS_NATIVE && (
87 <MinimalHeader
88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
89 profile={props.profile}
+3-3
src/screens/Profile/ProfileFeed/index.tsx
···17import {type NavigationProp} from '#/lib/routes/types'
18import {makeRecordUri} from '#/lib/strings/url-helpers'
19import {s} from '#/lib/styles'
20-import {isNative} from '#/platform/detection'
21import {listenSoftReset} from '#/state/events'
22import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
23import {
···47} from '#/screens/Profile/components/ProfileFeedHeader'
48import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
49import * as Layout from '#/components/Layout'
05051type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
52export function ProfileFeedScreen(props: Props) {
···175176 const onScrollToTop = useCallback(() => {
177 scrollElRef.current?.scrollToOffset({
178- animated: isNative,
179 offset: 0, // -headerHeight,
180 })
181 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···204 const feedIsVideoMode =
205 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
206 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
207- return isNative && _isVideoFeed
208 }, [feedInfo])
209210 return (
···17import {type NavigationProp} from '#/lib/routes/types'
18import {makeRecordUri} from '#/lib/strings/url-helpers'
19import {s} from '#/lib/styles'
020import {listenSoftReset} from '#/state/events'
21import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
22import {
···46} from '#/screens/Profile/components/ProfileFeedHeader'
47import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
48import * as Layout from '#/components/Layout'
49+import {IS_NATIVE} from '#/env'
5051type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
52export function ProfileFeedScreen(props: Props) {
···175176 const onScrollToTop = useCallback(() => {
177 scrollElRef.current?.scrollToOffset({
178+ animated: IS_NATIVE,
179 offset: 0, // -headerHeight,
180 })
181 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···204 const feedIsVideoMode =
205 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
206 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
207+ return IS_NATIVE && _isVideoFeed
208 }, [feedInfo])
209210 return (
+4-4
src/screens/Profile/Sections/Feed.tsx
···5import {useQueryClient} from '@tanstack/react-query'
67import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
8-import {isIOS, isNative} from '#/platform/detection'
9import {
10 type FeedDescriptor,
11 RQKEY as FEED_RQKEY,
···21import {atoms as a, ios, useTheme} from '#/alf'
22import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
23import {Text} from '#/components/Typography'
024import {type SectionRef} from './types'
2526interface FeedSectionProps {
···53 const [hasNew, setHasNew] = useState(false)
54 const [isScrolledDown, setIsScrolledDown] = useState(false)
55 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
56- const isVideoFeed = isNative && feed.endsWith('posts_with_video')
57 const adjustedInitialNumToRender = useInitialNumToRender({
58 screenHeightOffset: headerHeight,
59 })
60 const onScrollToTop = useCallback(() => {
61 scrollElRef.current?.scrollToOffset({
62- animated: isNative,
63 offset: -headerHeight,
64 })
65 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···85 }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
8687 useEffect(() => {
88- if (isIOS && isFocused && scrollElRef.current) {
89 const nativeTag = findNodeHandle(scrollElRef.current)
90 setScrollViewTag(nativeTag)
91 }
···5import {useQueryClient} from '@tanstack/react-query'
67import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
08import {
9 type FeedDescriptor,
10 RQKEY as FEED_RQKEY,
···20import {atoms as a, ios, useTheme} from '#/alf'
21import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
22import {Text} from '#/components/Typography'
23+import {IS_IOS, IS_NATIVE} from '#/env'
24import {type SectionRef} from './types'
2526interface FeedSectionProps {
···53 const [hasNew, setHasNew] = useState(false)
54 const [isScrolledDown, setIsScrolledDown] = useState(false)
55 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
56+ const isVideoFeed = IS_NATIVE && feed.endsWith('posts_with_video')
57 const adjustedInitialNumToRender = useInitialNumToRender({
58 screenHeightOffset: headerHeight,
59 })
60 const onScrollToTop = useCallback(() => {
61 scrollElRef.current?.scrollToOffset({
62+ animated: IS_NATIVE,
63 offset: -headerHeight,
64 })
65 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···85 }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
8687 useEffect(() => {
88+ if (IS_IOS && isFocused && scrollElRef.current) {
89 const nativeTag = findNodeHandle(scrollElRef.current)
90 setScrollViewTag(nativeTag)
91 }
+3-3
src/screens/Profile/Sections/Labels.tsx
···10import {useLingui} from '@lingui/react'
1112import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
13-import {isIOS, isNative} from '#/platform/detection'
14import {List, type ListRef} from '#/view/com/util/List'
15import {atoms as a, ios, tokens, useTheme} from '#/alf'
16import {Divider} from '#/components/Divider'
···19import {Loader} from '#/components/Loader'
20import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
21import {Text} from '#/components/Typography'
022import {ErrorState} from '../ErrorState'
23import {type SectionRef} from './types'
24···4950 const onScrollToTop = useCallback(() => {
51 scrollElRef.current?.scrollToOffset({
52- animated: isNative,
53 offset: -headerHeight,
54 })
55 }, [scrollElRef, headerHeight])
···59 }))
6061 useEffect(() => {
62- if (isIOS && isFocused && scrollElRef.current) {
63 const nativeTag = findNodeHandle(scrollElRef.current)
64 setScrollViewTag(nativeTag)
65 }
···10import {useLingui} from '@lingui/react'
1112import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
013import {List, type ListRef} from '#/view/com/util/List'
14import {atoms as a, ios, tokens, useTheme} from '#/alf'
15import {Divider} from '#/components/Divider'
···18import {Loader} from '#/components/Loader'
19import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
20import {Text} from '#/components/Typography'
21+import {IS_IOS, IS_NATIVE} from '#/env'
22import {ErrorState} from '../ErrorState'
23import {type SectionRef} from './types'
24···4950 const onScrollToTop = useCallback(() => {
51 scrollElRef.current?.scrollToOffset({
52+ animated: IS_NATIVE,
53 offset: -headerHeight,
54 })
55 }, [scrollElRef, headerHeight])
···59 }))
6061 useEffect(() => {
62+ if (IS_IOS && isFocused && scrollElRef.current) {
63 const nativeTag = findNodeHandle(scrollElRef.current)
64 setScrollViewTag(nativeTag)
65 }
···3import {Trans} from '@lingui/macro'
45import {logger} from '#/logger'
6-import {isWeb} from '#/platform/detection'
7import {
8 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
9 useTrendingTopics,
···17 TrendingTopicSkeleton,
18} from '#/components/TrendingTopics'
19import {Text} from '#/components/Typography'
02021// Note: This module is not currently used and may be removed in the future.
22···37 <View
38 style={[
39 a.flex_row,
40- isWeb
41 ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
42 : [a.p_lg, a.pt_2xl, a.gap_md],
43 a.border_b,
···3import {Trans} from '@lingui/macro'
45import {logger} from '#/logger'
06import {
7 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
8 useTrendingTopics,
···16 TrendingTopicSkeleton,
17} from '#/components/TrendingTopics'
18import {Text} from '#/components/Typography'
19+import {IS_WEB} from '#/env'
2021// Note: This module is not currently used and may be removed in the future.
22···37 <View
38 style={[
39 a.flex_row,
40+ IS_WEB
41 ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
42 : [a.p_lg, a.pt_2xl, a.gap_md],
43 a.border_b,
+4-4
src/screens/Settings/AboutSettings.tsx
···1112import {STATUS_PAGE_URL} from '#/lib/constants'
13import {type CommonNavigatorParams} from '#/lib/routes/types'
14-import {isAndroid, isIOS, isNative} from '#/platform/detection'
15import * as Toast from '#/view/com/util/Toast'
16import * as SettingsList from '#/screens/Settings/components/SettingsList'
17import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
···22import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
23import * as Layout from '#/components/Layout'
24import {Loader} from '#/components/Loader'
025import * as env from '#/env'
26import {useDemoMode} from '#/storage/hooks/demo-mode'
27import {useDevMode} from '#/storage/hooks/dev-mode'
···44 return spaceDiff * -1
45 },
46 onSuccess: sizeDiffBytes => {
47- if (isAndroid) {
48 Toast.show(
49 _(
50 msg({
···110 <Trans>System log</Trans>
111 </SettingsList.ItemText>
112 </SettingsList.LinkItem>
113- {isNative && (
114 <SettingsList.PressableItem
115 onPress={() => onClearImageCache()}
116 label={_(msg`Clear image cache`)}
···159 {devModeEnabled && (
160 <>
161 <OTAInfo />
162- {isIOS && (
163 <SettingsList.PressableItem
164 onPress={() => {
165 const newDemoModeEnabled = !demoModeEnabled
···1112import {STATUS_PAGE_URL} from '#/lib/constants'
13import {type CommonNavigatorParams} from '#/lib/routes/types'
014import * as Toast from '#/view/com/util/Toast'
15import * as SettingsList from '#/screens/Settings/components/SettingsList'
16import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
···21import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
22import * as Layout from '#/components/Layout'
23import {Loader} from '#/components/Loader'
24+import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env'
25import * as env from '#/env'
26import {useDemoMode} from '#/storage/hooks/demo-mode'
27import {useDevMode} from '#/storage/hooks/dev-mode'
···44 return spaceDiff * -1
45 },
46 onSuccess: sizeDiffBytes => {
47+ if (IS_ANDROID) {
48 Toast.show(
49 _(
50 msg({
···110 <Trans>System log</Trans>
111 </SettingsList.ItemText>
112 </SettingsList.LinkItem>
113+ {IS_NATIVE && (
114 <SettingsList.PressableItem
115 onPress={() => onClearImageCache()}
116 label={_(msg`Clear image cache`)}
···159 {devModeEnabled && (
160 <>
161 <OTAInfo />
162+ {IS_IOS && (
163 <SettingsList.PressableItem
164 onPress={() => {
165 const newDemoModeEnabled = !demoModeEnabled
+2-2
src/screens/Settings/AccessibilitySettings.tsx
···3import {type NativeStackScreenProps} from '@react-navigation/native-stack'
45import {type CommonNavigatorParams} from '#/lib/routes/types'
6-import {isNative} from '#/platform/detection'
7import {
8 useHapticsDisabled,
9 useRequireAltTextEnabled,
···20import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
21import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
22import * as Layout from '#/components/Layout'
02324type Props = NativeStackScreenProps<
25 CommonNavigatorParams,
···76 <Toggle.Platform />
77 </Toggle.Item>
78 </SettingsList.Group>
79- {isNative && (
80 <>
81 <SettingsList.Divider />
82 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
···3import {type NativeStackScreenProps} from '@react-navigation/native-stack'
45import {type CommonNavigatorParams} from '#/lib/routes/types'
06import {
7 useHapticsDisabled,
8 useRequireAltTextEnabled,
···19import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
20import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
21import * as Layout from '#/components/Layout'
22+import {IS_NATIVE} from '#/env'
2324type Props = NativeStackScreenProps<
25 CommonNavigatorParams,
···76 <Toggle.Platform />
77 </Toggle.Item>
78 </SettingsList.Group>
79+ {IS_NATIVE && (
80 <>
81 <SettingsList.Divider />
82 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
+3-3
src/screens/Settings/AppIconSettings/index.tsx
···8import {PressableScale} from '#/lib/custom-animations/PressableScale'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
10import {useGate} from '#/lib/statsig/statsig'
11-import {isAndroid} from '#/platform/detection'
12import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
13import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
14import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
···16import * as Toggle from '#/components/forms/Toggle'
17import * as Layout from '#/components/Layout'
18import {Text} from '#/components/Typography'
019import {IS_INTERNAL} from '#/env'
2021type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
···29 )
3031 const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
32- if (isAndroid) {
33 const next =
34 sets.defaults.find(i => i.id === icon) ??
35 sets.core.find(i => i.id === icon)
···221 accessibilityHint={_(msg`Changes app icon`)}
222 targetScale={0.95}
223 onPress={() => {
224- if (isAndroid) {
225 Alert.alert(
226 _(msg`Change app icon to "${icon.name}"`),
227 _(msg`The app will be restarted`),
···8import {PressableScale} from '#/lib/custom-animations/PressableScale'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
10import {useGate} from '#/lib/statsig/statsig'
011import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
12import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
13import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
···15import * as Toggle from '#/components/forms/Toggle'
16import * as Layout from '#/components/Layout'
17import {Text} from '#/components/Typography'
18+import {IS_ANDROID} from '#/env'
19import {IS_INTERNAL} from '#/env'
2021type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
···29 )
3031 const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
32+ if (IS_ANDROID) {
33 const next =
34 sets.defaults.find(i => i.id === icon) ??
35 sets.core.find(i => i.id === icon)
···221 accessibilityHint={_(msg`Changes app icon`)}
222 targetScale={0.95}
223 onPress={() => {
224+ if (IS_ANDROID) {
225 Alert.alert(
226 _(msg`Change app icon to "${icon.name}"`),
227 _(msg`The app will be restarted`),
+2-2
src/screens/Settings/AppearanceSettings.tsx
···12 type CommonNavigatorParams,
13 type NativeStackScreenProps,
14} from '#/lib/routes/types'
15-import {isNative} from '#/platform/detection'
16import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
17import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
18import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
···24import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
25import * as Layout from '#/components/Layout'
26import {Text} from '#/components/Typography'
027import {IS_INTERNAL} from '#/env'
28import * as SettingsList from './components/SettingsList'
29···165 onChange={onChangeFontScale}
166 />
167168- {isNative && IS_INTERNAL && (
169 <>
170 <SettingsList.Divider />
171 <AppIconSettingsListItem />
···12 type CommonNavigatorParams,
13 type NativeStackScreenProps,
14} from '#/lib/routes/types'
015import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
16import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
17import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
···23import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
24import * as Layout from '#/components/Layout'
25import {Text} from '#/components/Typography'
26+import {IS_NATIVE} from '#/env'
27import {IS_INTERNAL} from '#/env'
28import * as SettingsList from './components/SettingsList'
29···165 onChange={onChangeFontScale}
166 />
167168+ {IS_NATIVE && IS_INTERNAL && (
169 <>
170 <SettingsList.Divider />
171 <AppIconSettingsListItem />
+2-2
src/screens/Settings/ContentAndMediaSettings.tsx
···45import {type CommonNavigatorParams} from '#/lib/routes/types'
6import {logEvent} from '#/lib/statsig/statsig'
7-import {isNative} from '#/platform/detection'
8import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
9import {
10 useInAppBrowser,
···26import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
27import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
28import * as Layout from '#/components/Layout'
029import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle'
3031type Props = NativeStackScreenProps<
···97 </SettingsList.ItemText>
98 </SettingsList.LinkItem>
99 <SettingsList.Divider />
100- {isNative && (
101 <Toggle.Item
102 name="use_in_app_browser"
103 label={_(msg`Use in-app browser to open links`)}
···45import {type CommonNavigatorParams} from '#/lib/routes/types'
6import {logEvent} from '#/lib/statsig/statsig'
07import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences'
8import {
9 useInAppBrowser,
···25import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
26import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window'
27import * as Layout from '#/components/Layout'
28+import {IS_NATIVE} from '#/env'
29import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle'
3031type Props = NativeStackScreenProps<
···97 </SettingsList.ItemText>
98 </SettingsList.LinkItem>
99 <SettingsList.Divider />
100+ {IS_NATIVE && (
101 <Toggle.Item
102 name="use_in_app_browser"
103 label={_(msg`Use in-app browser to open links`)}
+2-2
src/screens/Settings/FindContactsSettings.tsx
···20} from '#/lib/routes/types'
21import {cleanError, isNetworkError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23-import {isNative} from '#/platform/detection'
24import {
25 updateProfileShadow,
26 useProfileShadow,
···48import * as ProfileCard from '#/components/ProfileCard'
49import * as Toast from '#/components/Toast'
50import {Text} from '#/components/Typography'
051import type * as bsky from '#/types/bsky'
52import {bulkWriteFollows} from '../Onboarding/util'
53···78 </Layout.Header.Content>
79 <Layout.Header.Slot />
80 </Layout.Header.Outer>
81- {isNative ? (
82 data ? (
83 !data.syncStatus ? (
84 <Intro />
···20} from '#/lib/routes/types'
21import {cleanError, isNetworkError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
023import {
24 updateProfileShadow,
25 useProfileShadow,
···47import * as ProfileCard from '#/components/ProfileCard'
48import * as Toast from '#/components/Toast'
49import {Text} from '#/components/Typography'
50+import {IS_NATIVE} from '#/env'
51import type * as bsky from '#/types/bsky'
52import {bulkWriteFollows} from '../Onboarding/util'
53···78 </Layout.Header.Content>
79 <Layout.Header.Slot />
80 </Layout.Header.Outer>
81+ {IS_NATIVE ? (
82 data ? (
83 !data.syncStatus ? (
84 <Intro />
···13import {useLingui} from '@lingui/react'
14import {useMutation} from '@tanstack/react-query'
1516-import {isWeb} from '#/platform/detection'
17import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
18import {atoms as a, native, useTheme} from '#/alf'
19import {Admonition} from '#/components/Admonition'
···24import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
25import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
26import {Text} from '#/components/Typography'
027import {CopyButton} from './CopyButton'
2829export function AddAppPasswordDialog({
···181 ) : (
182 <Animated.View
183 style={[a.gap_lg]}
184- entering={isWeb ? FadeIn.delay(200) : SlideInRight}
185 key={1}>
186 <Text style={[a.text_2xl, a.font_semi_bold]}>
187 <Trans>Here is your app password!</Trans>
···13import {useLingui} from '@lingui/react'
14import {useMutation} from '@tanstack/react-query'
15016import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
17import {atoms as a, native, useTheme} from '#/alf'
18import {Admonition} from '#/components/Admonition'
···23import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
24import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
25import {Text} from '#/components/Typography'
26+import {IS_WEB} from '#/env'
27import {CopyButton} from './CopyButton'
2829export function AddAppPasswordDialog({
···181 ) : (
182 <Animated.View
183 style={[a.gap_lg]}
184+ entering={IS_WEB ? FadeIn.delay(200) : SlideInRight}
185 key={1}>
186 <Text style={[a.text_2xl, a.font_semi_bold]}>
187 <Trans>Here is your app password!</Trans>
···7import {cleanError, isNetworkError} from '#/lib/strings/errors'
8import {checkAndFormatResetCode} from '#/lib/strings/password'
9import {logger} from '#/logger'
10-import {isNative} from '#/platform/detection'
11import {useAgent, useSession} from '#/state/session'
12import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
13import {android, atoms as a, web} from '#/alf'
···16import * as TextField from '#/components/forms/TextField'
17import {Loader} from '#/components/Loader'
18import {Text} from '#/components/Typography'
01920enum Stages {
21 RequestCode = 'RequestCode',
···242 <Trans>Already have a code?</Trans>
243 </ButtonText>
244 </Button>
245- {isNative && (
246 <Button
247 label={_(msg`Cancel`)}
248 color="secondary"
···7import {cleanError, isNetworkError} from '#/lib/strings/errors'
8import {checkAndFormatResetCode} from '#/lib/strings/password'
9import {logger} from '#/logger'
010import {useAgent, useSession} from '#/state/session'
11import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
12import {android, atoms as a, web} from '#/alf'
···15import * as TextField from '#/components/forms/TextField'
16import {Loader} from '#/components/Loader'
17import {Text} from '#/components/Typography'
18+import {IS_NATIVE} from '#/env'
1920enum Stages {
21 RequestCode = 'RequestCode',
···242 <Trans>Already have a code?</Trans>
243 </ButtonText>
244 </Button>
245+ {IS_NATIVE && (
246 <Button
247 label={_(msg`Cancel`)}
248 color="secondary"
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
7-import {isNative} from '#/platform/detection'
8import {useAgent, useSession} from '#/state/session'
9import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
10import * as Toast from '#/view/com/util/Toast'
···15import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
16import {Loader} from '#/components/Loader'
17import {P, Text} from '#/components/Typography'
01819enum Stages {
20 Email,
···193 </View>
194 ) : undefined}
195196- {!gtMobile && isNative && <View style={{height: 40}} />}
197 </View>
198 </Dialog.ScrollableInner>
199 </Dialog.Outer>
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
07import {useAgent, useSession} from '#/state/session'
8import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
9import * as Toast from '#/view/com/util/Toast'
···14import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
15import {Loader} from '#/components/Loader'
16import {P, Text} from '#/components/Typography'
17+import {IS_NATIVE} from '#/env'
1819enum Stages {
20 Email,
···193 </View>
194 ) : undefined}
195196+ {!gtMobile && IS_NATIVE && <View style={{height: 40}} />}
197 </View>
198 </Dialog.ScrollableInner>
199 </Dialog.Outer>
+8-6
src/screens/Signup/StepCaptcha/index.tsx
···78import {createFullHandle} from '#/lib/strings/handles'
9import {logger} from '#/logger'
10-import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
11import {useSignupContext} from '#/screens/Signup/state'
12import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
13import {atoms as a, useTheme} from '#/alf'
14import {FormError} from '#/components/forms/FormError'
015import {GCP_PROJECT_ID} from '#/env'
16import {BackNextButtons} from '../BackNextButtons'
1718const CAPTCHA_PATH =
19- isWeb || GCP_PROJECT_ID === 0 ? '/gate/signup' : '/gate/signup/attempt-attest'
002021export function StepCaptcha() {
22- if (isWeb) {
23 return <StepCaptchaInner />
24 } else {
25 return <StepCaptchaNative />
···35 ;(async () => {
36 logger.debug('trying to generate attestation token...')
37 try {
38- if (isIOS) {
39 logger.debug('starting to generate devicecheck token...')
40 const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
41 setToken(token)
···85 newUrl.searchParams.set('state', stateParam)
86 newUrl.searchParams.set('colorScheme', theme.name)
8788- if (isNative && token) {
89 newUrl.searchParams.set('platform', Platform.OS)
90 newUrl.searchParams.set('token', token)
91- if (isAndroid && payload) {
92 newUrl.searchParams.set('payload', payload)
93 }
94 }
···78import {createFullHandle} from '#/lib/strings/handles'
9import {logger} from '#/logger'
010import {useSignupContext} from '#/screens/Signup/state'
11import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
12import {atoms as a, useTheme} from '#/alf'
13import {FormError} from '#/components/forms/FormError'
14+import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
15import {GCP_PROJECT_ID} from '#/env'
16import {BackNextButtons} from '../BackNextButtons'
1718const CAPTCHA_PATH =
19+ IS_WEB || GCP_PROJECT_ID === 0
20+ ? '/gate/signup'
21+ : '/gate/signup/attempt-attest'
2223export function StepCaptcha() {
24+ if (IS_WEB) {
25 return <StepCaptchaInner />
26 } else {
27 return <StepCaptchaNative />
···37 ;(async () => {
38 logger.debug('trying to generate attestation token...')
39 try {
40+ if (IS_IOS) {
41 logger.debug('starting to generate devicecheck token...')
42 const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
43 setToken(token)
···87 newUrl.searchParams.set('state', stateParam)
88 newUrl.searchParams.set('colorScheme', theme.name)
8990+ if (IS_NATIVE && token) {
91 newUrl.searchParams.set('platform', Platform.OS)
92 newUrl.searchParams.set('token', token)
93+ if (IS_ANDROID && payload) {
94 newUrl.searchParams.set('payload', payload)
95 }
96 }
+3-3
src/screens/Signup/StepInfo/index.tsx
···78import {isEmailMaybeInvalid} from '#/lib/strings/email'
9import {logger} from '#/logger'
10-import {isNative} from '#/platform/detection'
11import {useSignupContext} from '#/screens/Signup/state'
12import {Policies} from '#/screens/Signup/StepInfo/Policies'
13import {atoms as a, native} from '#/alf'
···31 MIN_ACCESS_AGE,
32 useAgeAssuranceRegionConfigWithFallback,
33} from '#/ageAssurance/util'
034import {
35 useDeviceGeolocationApi,
36 useIsDeviceGeolocationGranted,
···325 </Trans>
326 )}
327 </Admonition.Text>
328- {isNative &&
329 !isDeviceGeolocationGranted &&
330 isOverAppMinAccessAge && (
331 <Admonition.Text>
···357 ) : undefined}
358 </View>
359360- {isNative && (
361 <DeviceLocationRequestDialog
362 control={locationControl}
363 onLocationAcquired={props => {
···78import {isEmailMaybeInvalid} from '#/lib/strings/email'
9import {logger} from '#/logger'
010import {useSignupContext} from '#/screens/Signup/state'
11import {Policies} from '#/screens/Signup/StepInfo/Policies'
12import {atoms as a, native} from '#/alf'
···30 MIN_ACCESS_AGE,
31 useAgeAssuranceRegionConfigWithFallback,
32} from '#/ageAssurance/util'
33+import {IS_NATIVE} from '#/env'
34import {
35 useDeviceGeolocationApi,
36 useIsDeviceGeolocationGranted,
···325 </Trans>
326 )}
327 </Admonition.Text>
328+ {IS_NATIVE &&
329 !isDeviceGeolocationGranted &&
330 isOverAppMinAccessAge && (
331 <Admonition.Text>
···357 ) : undefined}
358 </View>
359360+ {IS_NATIVE && (
361 <DeviceLocationRequestDialog
362 control={locationControl}
363 onLocationAcquired={props => {
+2-2
src/screens/Signup/index.tsx
···89import {FEEDBACK_FORM_URL} from '#/lib/constants'
10import {logger} from '#/logger'
11-import {isAndroid} from '#/platform/detection'
12import {useServiceQuery} from '#/state/queries/service'
13import {useStarterPackQuery} from '#/state/queries/starter-packs'
14import {useActiveStarterPack} from '#/state/shell/starter-pack'
···30import {InlineLinkText} from '#/components/Link'
31import {ScreenTransition} from '#/components/ScreenTransition'
32import {Text} from '#/components/Typography'
033import {GCP_PROJECT_ID} from '#/env'
34import * as bsky from '#/types/bsky'
35···108109 // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen.
110 useEffect(() => {
111- if (!isAndroid) {
112 return
113 }
114 ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err =>
···89import {FEEDBACK_FORM_URL} from '#/lib/constants'
10import {logger} from '#/logger'
011import {useServiceQuery} from '#/state/queries/service'
12import {useStarterPackQuery} from '#/state/queries/starter-packs'
13import {useActiveStarterPack} from '#/state/shell/starter-pack'
···29import {InlineLinkText} from '#/components/Link'
30import {ScreenTransition} from '#/components/ScreenTransition'
31import {Text} from '#/components/Typography'
32+import {IS_ANDROID} from '#/env'
33import {GCP_PROJECT_ID} from '#/env'
34import * as bsky from '#/types/bsky'
35···108109 // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen.
110 useEffect(() => {
111+ if (!IS_ANDROID) {
112 return
113 }
114 ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err =>
+3-3
src/screens/SignupQueued.tsx
···6import {useLingui} from '@lingui/react'
78import {logger} from '#/logger'
9-import {isIOS, isWeb} from '#/platform/detection'
10import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
11import {useOnboardingDispatch} from '#/state/shell'
12import {Logo} from '#/view/icons/Logo'
···14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import {Loader} from '#/components/Loader'
16import {P, Text} from '#/components/Typography'
01718const COL_WIDTH = 400
19···98 </Button>
99 )
100101- const webLayout = isWeb && gtMobile
102103 return (
104 <Modal
···106 animationType={native('slide')}
107 presentationStyle="formSheet"
108 style={[web(a.util_screen_outer)]}>
109- {isIOS && <SystemBars style={{statusBar: 'light'}} />}
110 <ScrollView
111 style={[a.flex_1, t.atoms.bg]}
112 contentContainerStyle={{borderWidth: 0}}
···6import {useLingui} from '@lingui/react'
78import {logger} from '#/logger'
09import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
10import {useOnboardingDispatch} from '#/state/shell'
11import {Logo} from '#/view/icons/Logo'
···13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import {Loader} from '#/components/Loader'
15import {P, Text} from '#/components/Typography'
16+import {IS_IOS, IS_WEB} from '#/env'
1718const COL_WIDTH = 400
19···98 </Button>
99 )
100101+ const webLayout = IS_WEB && gtMobile
102103 return (
104 <Modal
···106 animationType={native('slide')}
107 presentationStyle="formSheet"
108 style={[web(a.util_screen_outer)]}>
109+ {IS_IOS && <SystemBars style={{statusBar: 'light'}} />}
110 <ScrollView
111 style={[a.flex_1, t.atoms.bg]}
112 contentContainerStyle={{borderWidth: 0}}
···1import React from 'react'
23-import {isWeb} from '#/platform/detection'
4import {type DialogControlRefProps} from '#/components/Dialog'
5import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
06import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet'
78interface IDialogContext {
···62 const openDialogs = React.useRef<Set<string>>(new Set())
6364 const closeAllDialogs = React.useCallback(() => {
65- if (isWeb) {
66 openDialogs.current.forEach(id => {
67 const dialog = activeDialogs.current.get(id)
68 if (dialog) dialog.current.close()
···1import React from 'react'
203import {type DialogControlRefProps} from '#/components/Dialog'
4import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
5+import {IS_WEB} from '#/env'
6import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet'
78interface IDialogContext {
···62 const openDialogs = React.useRef<Set<string>>(new Set())
6364 const closeAllDialogs = React.useCallback(() => {
65+ if (IS_WEB) {
66 openDialogs.current.forEach(id => {
67 const dialog = activeDialogs.current.get(id)
68 if (dialog) dialog.current.close()
+5-5
src/state/gallery.ts
···18import {type PickerImage} from '#/lib/media/picker.shared'
19import {getDataUriSize} from '#/lib/media/util'
20import {isCancelledError} from '#/lib/strings/errors'
21-import {isNative} from '#/platform/detection'
2223export type ImageTransformation = {
24 crop?: ActionCrop['crop']
···55let _imageCacheDirectory: string
5657function getImageCacheDirectory(): string | null {
58- if (isNative) {
59 return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer'))
60 }
61···120}
121122export async function cropImage(img: ComposerImage): Promise<ComposerImage> {
123- if (!isNative) {
124 return img
125 }
126···244}
245246async function moveIfNecessary(from: string) {
247- const cacheDir = isNative && getImageCacheDirectory()
248249 if (cacheDir && from.startsWith(cacheDir)) {
250 const to = joinPath(cacheDir, nanoid(36))
···260261/** Purge files that were created to accomodate image manipulation */
262export async function purgeTemporaryImageFiles() {
263- const cacheDir = isNative && getImageCacheDirectory()
264265 if (cacheDir) {
266 await deleteAsync(cacheDir, {idempotent: true})
···18import {type PickerImage} from '#/lib/media/picker.shared'
19import {getDataUriSize} from '#/lib/media/util'
20import {isCancelledError} from '#/lib/strings/errors'
21+import {IS_NATIVE} from '#/env'
2223export type ImageTransformation = {
24 crop?: ActionCrop['crop']
···55let _imageCacheDirectory: string
5657function getImageCacheDirectory(): string | null {
58+ if (IS_NATIVE) {
59 return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer'))
60 }
61···120}
121122export async function cropImage(img: ComposerImage): Promise<ComposerImage> {
123+ if (!IS_NATIVE) {
124 return img
125 }
126···244}
245246async function moveIfNecessary(from: string) {
247+ const cacheDir = IS_NATIVE && getImageCacheDirectory()
248249 if (cacheDir && from.startsWith(cacheDir)) {
250 const to = joinPath(cacheDir, nanoid(36))
···260261/** Purge files that were created to accomodate image manipulation */
262export async function purgeTemporaryImageFiles() {
263+ const cacheDir = IS_NATIVE && getImageCacheDirectory()
264265 if (cacheDir) {
266 await deleteAsync(cacheDir, {idempotent: true})
+2-2
src/state/messages/convo/agent.ts
···16 isNetworkError,
17} from '#/lib/strings/errors'
18import {Logger} from '#/logger'
19-import {isNative} from '#/platform/detection'
20import {
21 ACTIVE_POLL_INTERVAL,
22 BACKGROUND_POLL_INTERVAL,
···37} from '#/state/messages/convo/types'
38import {type MessagesEventBus} from '#/state/messages/events/agent'
39import {type MessagesEventBusError} from '#/state/messages/events/types'
04041const logger = Logger.create(Logger.Context.ConversationAgent)
42···639 {
640 cursor: nextCursor,
641 convoId: this.convoId,
642- limit: isNative ? 30 : 60,
643 },
644 {headers: DM_SERVICE_HEADERS},
645 )
···16 isNetworkError,
17} from '#/lib/strings/errors'
18import {Logger} from '#/logger'
019import {
20 ACTIVE_POLL_INTERVAL,
21 BACKGROUND_POLL_INTERVAL,
···36} from '#/state/messages/convo/types'
37import {type MessagesEventBus} from '#/state/messages/events/agent'
38import {type MessagesEventBusError} from '#/state/messages/events/types'
39+import {IS_NATIVE} from '#/env'
4041const logger = Logger.create(Logger.Context.ConversationAgent)
42···639 {
640 cursor: nextCursor,
641 convoId: this.convoId,
642+ limit: IS_NATIVE ? 30 : 60,
643 },
644 {headers: DM_SERVICE_HEADERS},
645 )
+2-2
src/state/preferences/kawaii.tsx
···1import React from 'react'
23-import {isWeb} from '#/platform/detection'
4import * as persisted from '#/state/persisted'
056type StateContext = persisted.Schema['kawaii']
7···30 React.useEffect(() => {
31 // dumb and stupid but it's web only so just refresh the page if you want to change it
3233- if (isWeb) {
34 const kawaii = new URLSearchParams(window.location.search).get('kawaii')
35 switch (kawaii) {
36 case 'true':
···1import React from 'react'
203import * as persisted from '#/state/persisted'
4+import {IS_WEB} from '#/env'
56type StateContext = persisted.Schema['kawaii']
7···30 React.useEffect(() => {
31 // dumb and stupid but it's web only so just refresh the page if you want to change it
3233+ if (IS_WEB) {
34 const kawaii = new URLSearchParams(window.location.search).get('kawaii')
35 switch (kawaii) {
36 case 'true':
+2-2
src/state/queries/usePostThread/index.ts
···1import {useCallback, useMemo, useState} from 'react'
2import {useQuery, useQueryClient} from '@tanstack/react-query'
34-import {isWeb} from '#/platform/detection'
5import {useModerationOpts} from '#/state/preferences/moderation-opts'
6import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
7import {
···31import {useAgent, useSession} from '#/state/session'
32import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
33import {useBreakpoints} from '#/alf'
03435export * from '#/state/queries/usePostThread/context'
36export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
···53 const below = useMemo(() => {
54 return view === 'linear'
55 ? LINEAR_VIEW_BELOW
56- : isWeb && gtPhone
57 ? TREE_VIEW_BELOW_DESKTOP
58 : TREE_VIEW_BELOW
59 }, [view, gtPhone])
···1import {useCallback, useMemo, useState} from 'react'
2import {useQuery, useQueryClient} from '@tanstack/react-query'
304import {useModerationOpts} from '#/state/preferences/moderation-opts'
5import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences'
6import {
···30import {useAgent, useSession} from '#/state/session'
31import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
32import {useBreakpoints} from '#/alf'
33+import {IS_WEB} from '#/env'
3435export * from '#/state/queries/usePostThread/context'
36export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache'
···53 const below = useMemo(() => {
54 return view === 'linear'
55 ? LINEAR_VIEW_BELOW
56+ : IS_WEB && gtPhone
57 ? TREE_VIEW_BELOW_DESKTOP
58 : TREE_VIEW_BELOW
59 }, [view, gtPhone])
+2-2
src/state/session/index.tsx
···1import React from 'react'
2import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
34-import {isWeb} from '#/platform/detection'
5import * as persisted from '#/state/persisted'
6import {useCloseAllActiveElements} from '#/state/util'
7import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
08import {emitSessionDropped} from '../events'
9import {
10 agentToSessionAccount,
···340 )
341342 // @ts-expect-error window type is not declared, debug only
343- if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent
344345 const agent = state.currentAgentState.agent as BskyAppAgent
346 const currentAgentRef = React.useRef(agent)
···1import React from 'react'
2import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
304import * as persisted from '#/state/persisted'
5import {useCloseAllActiveElements} from '#/state/util'
6import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
7+import {IS_WEB} from '#/env'
8import {emitSessionDropped} from '../events'
9import {
10 agentToSessionAccount,
···340 )
341342 // @ts-expect-error window type is not declared, debug only
343+ if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent
344345 const agent = state.currentAgentState.agent as BskyAppAgent
346 const currentAgentRef = React.useRef(agent)
+2-2
src/state/shell/logged-out.tsx
···1import React from 'react'
23-import {isWeb} from '#/platform/detection'
4import {useSession} from '#/state/session'
5import {useActiveStarterPack} from '#/state/shell/starter-pack'
067type State = {
8 showLoggedOut: boolean
···55 const [state, setState] = React.useState<State>({
56 showLoggedOut: shouldShowStarterPack,
57 requestedAccountSwitchTo: shouldShowStarterPack
58- ? isWeb
59 ? 'starterpack'
60 : 'new'
61 : undefined,
···1import React from 'react'
203import {useSession} from '#/state/session'
4import {useActiveStarterPack} from '#/state/shell/starter-pack'
5+import {IS_WEB} from '#/env'
67type State = {
8 showLoggedOut: boolean
···55 const [state, setState] = React.useState<State>({
56 showLoggedOut: shouldShowStarterPack,
57 requestedAccountSwitchTo: shouldShowStarterPack
58+ ? IS_WEB
59 ? 'starterpack'
60 : 'new'
61 : undefined,
+3-3
src/state/shell/selected-feed.tsx
···1import {createContext, useCallback, useContext, useState} from 'react'
23-import {isWeb} from '#/platform/detection'
4import {type FeedDescriptor} from '#/state/queries/post-feed'
5import {useSession} from '#/state/session'
06import {account} from '#/storage'
78type StateContext = FeedDescriptor | null
···14setContext.displayName = 'SelectedFeedSetContext'
1516function getInitialFeed(did?: string): FeedDescriptor | null {
17- if (isWeb) {
18 if (window.location.pathname === '/') {
19 const params = new URLSearchParams(window.location.search)
20 const feedFromUrl = params.get('feed')
···49 const saveState = useCallback(
50 (feed: FeedDescriptor) => {
51 setState(feed)
52- if (isWeb) {
53 try {
54 sessionStorage.setItem('lastSelectedHomeFeed', feed)
55 } catch {}
···1import {createContext, useCallback, useContext, useState} from 'react'
203import {type FeedDescriptor} from '#/state/queries/post-feed'
4import {useSession} from '#/state/session'
5+import {IS_WEB} from '#/env'
6import {account} from '#/storage'
78type StateContext = FeedDescriptor | null
···14setContext.displayName = 'SelectedFeedSetContext'
1516function getInitialFeed(did?: string): FeedDescriptor | null {
17+ if (IS_WEB) {
18 if (window.location.pathname === '/') {
19 const params = new URLSearchParams(window.location.search)
20 const feedFromUrl = params.get('feed')
···49 const saveState = useCallback(
50 (feed: FeedDescriptor) => {
51 setState(feed)
52+ if (IS_WEB) {
53 try {
54 sessionStorage.setItem('lastSelectedHomeFeed', feed)
55 } catch {}
···76import {cleanError} from '#/lib/strings/errors'
77import {colors} from '#/lib/styles'
78import {logger} from '#/logger'
79-import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
80import {useDialogStateControlContext} from '#/state/dialogs'
81import {emitPostCreated} from '#/state/events'
82import {
···130import * as Prompt from '#/components/Prompt'
131import * as Toast from '#/components/Toast'
132import {Text as NewText} from '#/components/Typography'
0133import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
134import {PostLanguageSelect} from './select-language/PostLanguageSelect'
135import {
···329 const insets = useSafeAreaInsets()
330 const viewStyles = useMemo(
331 () => ({
332- paddingTop: isAndroid ? insets.top : 0,
333 paddingBottom:
334 // iOS - when keyboard is closed, keep the bottom bar in the safe area
335- (isIOS && !isKeyboardVisible) ||
336 // Android - Android >=35 KeyboardAvoidingView adds double padding when
337 // keyboard is closed, so we subtract that in the offset and add it back
338 // here when the keyboard is open
339- (isAndroid && isKeyboardVisible)
340 ? insets.bottom
341 : 0,
342 }),
···366367 // On Android, pressing Back should ask confirmation.
368 useEffect(() => {
369- if (!isAndroid) {
370 return
371 }
372 const backHandler = BackHandler.addEventListener(
···671 composerState.mutableNeedsFocusActive = false
672 // On Android, this risks getting the cursor stuck behind the keyboard.
673 // Not worth it.
674- if (!isAndroid) {
675 textInput.current?.focus()
676 }
677 }
···727 </>
728 )
729730- const isWebFooterSticky = !isNative && thread.posts.length > 1
731 return (
732 <BottomSheetPortalProvider>
733 <KeyboardAvoidingView
734 testID="composePostView"
735- behavior={isIOS ? 'padding' : 'height'}
736 keyboardVerticalOffset={keyboardVerticalOffset}
737 style={a.flex_1}>
738 <View
···790 onPublish={onComposerPostPublish}
791 onError={setError}
792 />
793- {isWebFooterSticky && post.id === activePost.id && (
794 <View style={styles.stickyFooterWeb}>{footer}</View>
795 )}
796 </React.Fragment>
797 ))}
798 </Animated.ScrollView>
799- {!isWebFooterSticky && footer}
800 </View>
801802 <Prompt.Basic
···849 const {data: currentProfile} = useProfileQuery({did: currentDid})
850 const richtext = post.richtext
851 const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
852- const forceMinHeight = isWeb && isTextOnly && isActive
853 const selectTextInputPlaceholder = isReply
854 ? isFirstPost
855 ? _(msg`Write your reply`)
···889 async (uri: string) => {
890 if (
891 uri.startsWith('data:video/') ||
892- (isWeb && uri.startsWith('data:image/gif'))
893 ) {
894- if (isNative) return // web only
895 const [mimeType] = uri.slice('data:'.length).split(';')
896 if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
897 Toast.show(_(msg`Unsupported video type: ${mimeType}`), {
···921 a.mb_sm,
922 !isActive && isLastPost && a.mb_lg,
923 !isActive && styles.inactivePost,
924- isTextOnly && isNative && a.flex_grow,
925 ]}>
926- <View style={[a.flex_row, isNative && a.flex_1]}>
927 <UserAvatar
928 avatar={currentProfile?.avatar}
929 size={42}
···1241 </LayoutAnimationConfig>
1242 {embed.quote?.uri ? (
1243 <View
1244- style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}>
1245 <View style={[a.relative]}>
1246 <View style={{pointerEvents: 'none'}}>
1247 <LazyQuoteEmbed uri={embed.quote.uri} />
···1652 const {top, bottom} = useSafeAreaInsets()
16531654 // Android etc
1655- if (!isIOS) {
1656 // need to account for the edge-to-edge nav bar
1657 return bottom * -1
1658 }
···1696 const appState = useAppState()
16971698 useEffect(() => {
1699- if (isIOS) {
1700 if (appState === 'inactive') {
1701 Keyboard.dismiss()
1702 }
···1846 style: StyleProp<ViewStyle>
1847 children: React.ReactNode
1848}) {
1849- if (isWeb) return children
1850 return (
1851 <Animated.View
1852 style={style}
···76import {cleanError} from '#/lib/strings/errors'
77import {colors} from '#/lib/styles'
78import {logger} from '#/logger'
079import {useDialogStateControlContext} from '#/state/dialogs'
80import {emitPostCreated} from '#/state/events'
81import {
···129import * as Prompt from '#/components/Prompt'
130import * as Toast from '#/components/Toast'
131import {Text as NewText} from '#/components/Typography'
132+import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
133import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
134import {PostLanguageSelect} from './select-language/PostLanguageSelect'
135import {
···329 const insets = useSafeAreaInsets()
330 const viewStyles = useMemo(
331 () => ({
332+ paddingTop: IS_ANDROID ? insets.top : 0,
333 paddingBottom:
334 // iOS - when keyboard is closed, keep the bottom bar in the safe area
335+ (IS_IOS && !isKeyboardVisible) ||
336 // Android - Android >=35 KeyboardAvoidingView adds double padding when
337 // keyboard is closed, so we subtract that in the offset and add it back
338 // here when the keyboard is open
339+ (IS_ANDROID && isKeyboardVisible)
340 ? insets.bottom
341 : 0,
342 }),
···366367 // On Android, pressing Back should ask confirmation.
368 useEffect(() => {
369+ if (!IS_ANDROID) {
370 return
371 }
372 const backHandler = BackHandler.addEventListener(
···671 composerState.mutableNeedsFocusActive = false
672 // On Android, this risks getting the cursor stuck behind the keyboard.
673 // Not worth it.
674+ if (!IS_ANDROID) {
675 textInput.current?.focus()
676 }
677 }
···727 </>
728 )
729730+ const IS_WEBFooterSticky = !IS_NATIVE && thread.posts.length > 1
731 return (
732 <BottomSheetPortalProvider>
733 <KeyboardAvoidingView
734 testID="composePostView"
735+ behavior={IS_IOS ? 'padding' : 'height'}
736 keyboardVerticalOffset={keyboardVerticalOffset}
737 style={a.flex_1}>
738 <View
···790 onPublish={onComposerPostPublish}
791 onError={setError}
792 />
793+ {IS_WEBFooterSticky && post.id === activePost.id && (
794 <View style={styles.stickyFooterWeb}>{footer}</View>
795 )}
796 </React.Fragment>
797 ))}
798 </Animated.ScrollView>
799+ {!IS_WEBFooterSticky && footer}
800 </View>
801802 <Prompt.Basic
···849 const {data: currentProfile} = useProfileQuery({did: currentDid})
850 const richtext = post.richtext
851 const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
852+ const forceMinHeight = IS_WEB && isTextOnly && isActive
853 const selectTextInputPlaceholder = isReply
854 ? isFirstPost
855 ? _(msg`Write your reply`)
···889 async (uri: string) => {
890 if (
891 uri.startsWith('data:video/') ||
892+ (IS_WEB && uri.startsWith('data:image/gif'))
893 ) {
894+ if (IS_NATIVE) return // web only
895 const [mimeType] = uri.slice('data:'.length).split(';')
896 if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
897 Toast.show(_(msg`Unsupported video type: ${mimeType}`), {
···921 a.mb_sm,
922 !isActive && isLastPost && a.mb_lg,
923 !isActive && styles.inactivePost,
924+ isTextOnly && IS_NATIVE && a.flex_grow,
925 ]}>
926+ <View style={[a.flex_row, IS_NATIVE && a.flex_1]}>
927 <UserAvatar
928 avatar={currentProfile?.avatar}
929 size={42}
···1241 </LayoutAnimationConfig>
1242 {embed.quote?.uri ? (
1243 <View
1244+ style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], IS_WEB && [a.pb_md]]}>
1245 <View style={[a.relative]}>
1246 <View style={{pointerEvents: 'none'}}>
1247 <LazyQuoteEmbed uri={embed.quote.uri} />
···1652 const {top, bottom} = useSafeAreaInsets()
16531654 // Android etc
1655+ if (!IS_IOS) {
1656 // need to account for the edge-to-edge nav bar
1657 return bottom * -1
1658 }
···1696 const appState = useAppState()
16971698 useEffect(() => {
1699+ if (IS_IOS) {
1700 if (appState === 'inactive') {
1701 Keyboard.dismiss()
1702 }
···1846 style: StyleProp<ViewStyle>
1847 children: React.ReactNode
1848}) {
1849+ if (IS_WEB) return children
1850 return (
1851 <Animated.View
1852 style={style}
+2-2
src/view/com/composer/GifAltText.tsx
···9 type EmbedPlayerParams,
10 parseEmbedPlayerFromUrl,
11} from '#/lib/strings/embed-player'
12-import {isAndroid} from '#/platform/detection'
13import {useResolveGifQuery} from '#/state/queries/resolve-link'
14import {type Gif} from '#/state/queries/tenor'
15import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
···23import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
24import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
25import {Text} from '#/components/Typography'
026import {AltTextReminder} from './photos/Gallery'
2728export function GifAltTextDialog({
···224 </View>
225 <Dialog.Close />
226 {/* Maybe fix this later -h */}
227- {isAndroid ? <View style={{height: 300}} /> : null}
228 </Dialog.ScrollableInner>
229 )
230}
···9 type EmbedPlayerParams,
10 parseEmbedPlayerFromUrl,
11} from '#/lib/strings/embed-player'
012import {useResolveGifQuery} from '#/state/queries/resolve-link'
13import {type Gif} from '#/state/queries/tenor'
14import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
···22import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
23import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
24import {Text} from '#/components/Typography'
25+import {IS_ANDROID} from '#/env'
26import {AltTextReminder} from './photos/Gallery'
2728export function GifAltTextDialog({
···224 </View>
225 <Dialog.Close />
226 {/* Maybe fix this later -h */}
227+ {IS_ANDROID ? <View style={{height: 300}} /> : null}
228 </Dialog.ScrollableInner>
229 )
230}
+2-2
src/view/com/composer/KeyboardAccessory.tsx
···3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import type React from 'react'
56-import {isWeb} from '#/platform/detection'
7import {atoms as a, useTheme} from '#/alf'
089export function KeyboardAccessory({children}: {children: React.ReactNode}) {
10 const t = useTheme()
···22 ]
2324 // todo: when iPad support is added, it should also not use the KeyboardStickyView
25- if (isWeb) {
26 return <View style={style}>{children}</View>
27 }
28
···3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import type React from 'react'
506import {atoms as a, useTheme} from '#/alf'
7+import {IS_WEB} from '#/env'
89export function KeyboardAccessory({children}: {children: React.ReactNode}) {
10 const t = useTheme()
···22 ]
2324 // todo: when iPad support is added, it should also not use the KeyboardStickyView
25+ if (IS_WEB) {
26 return <View style={style}>{children}</View>
27 }
28
+9-9
src/view/com/composer/SelectMediaButton.tsx
···11} from '#/lib/hooks/usePermissions'
12import {openUnifiedPicker} from '#/lib/media/picker'
13import {extractDataUriMime} from '#/lib/media/util'
14-import {isNative, isWeb} from '#/platform/detection'
15import {MAX_IMAGES} from '#/view/com/composer/state/composer'
16import {atoms as a, useTheme} from '#/alf'
17import {Button} from '#/components/Button'
18import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
19import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
20import * as toast from '#/components/Toast'
02122export type SelectMediaButtonProps = {
23 disabled?: boolean
···91 'image/svg+xml',
92 'image/webp',
93 'image/avif',
94- isNative && 'image/heic',
95 ] as const
96).filter(Boolean)
97type SupportedImageMimeType = Exclude<
···261 * We don't care too much about mimeType at this point on native,
262 * since the `processVideo` step later on will convert to `.mp4`.
263 */
264- if (isWeb && !isSupportedVideoMimeType(mimeType)) {
265 errors.add(SelectedAssetError.Unsupported)
266 continue
267 }
···271 * to filter out large files on web. On native, we compress these anyway,
272 * so we only check on web.
273 */
274- if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
275 errors.add(SelectedAssetError.FileTooBig)
276 continue
277 }
···290 * to filter out large files on web. On native, we compress GIFs as
291 * videos anyway, so we only check on web.
292 */
293- if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
294 errors.add(SelectedAssetError.FileTooBig)
295 continue
296 }
···308 * base64 data-uri, so we construct it here for web only.
309 */
310 uri:
311- isWeb && asset.base64
312 ? `data:${mimeType};base64,${asset.base64}`
313 : asset.uri,
314 })
···327 }
328329 if (supportedAssets[0].duration) {
330- if (isWeb) {
331 /*
332 * Web reports duration as seconds
333 */
···432 )
433434 const onPressSelectMedia = useCallback(async () => {
435- if (isNative) {
436 const [photoAccess, videoAccess] = await Promise.all([
437 requestPhotoAccessIfNeeded(),
438 requestVideoAccessIfNeeded(),
···446 }
447 }
448449- if (isNative && Keyboard.isVisible()) {
450 Keyboard.dismiss()
451 }
452
···11} from '#/lib/hooks/usePermissions'
12import {openUnifiedPicker} from '#/lib/media/picker'
13import {extractDataUriMime} from '#/lib/media/util'
014import {MAX_IMAGES} from '#/view/com/composer/state/composer'
15import {atoms as a, useTheme} from '#/alf'
16import {Button} from '#/components/Button'
17import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
18import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
19import * as toast from '#/components/Toast'
20+import {IS_NATIVE, IS_WEB} from '#/env'
2122export type SelectMediaButtonProps = {
23 disabled?: boolean
···91 'image/svg+xml',
92 'image/webp',
93 'image/avif',
94+ IS_NATIVE && 'image/heic',
95 ] as const
96).filter(Boolean)
97type SupportedImageMimeType = Exclude<
···261 * We don't care too much about mimeType at this point on native,
262 * since the `processVideo` step later on will convert to `.mp4`.
263 */
264+ if (IS_WEB && !isSupportedVideoMimeType(mimeType)) {
265 errors.add(SelectedAssetError.Unsupported)
266 continue
267 }
···271 * to filter out large files on web. On native, we compress these anyway,
272 * so we only check on web.
273 */
274+ if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
275 errors.add(SelectedAssetError.FileTooBig)
276 continue
277 }
···290 * to filter out large files on web. On native, we compress GIFs as
291 * videos anyway, so we only check on web.
292 */
293+ if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
294 errors.add(SelectedAssetError.FileTooBig)
295 continue
296 }
···308 * base64 data-uri, so we construct it here for web only.
309 */
310 uri:
311+ IS_WEB && asset.base64
312 ? `data:${mimeType};base64,${asset.base64}`
313 : asset.uri,
314 })
···327 }
328329 if (supportedAssets[0].duration) {
330+ if (IS_WEB) {
331 /*
332 * Web reports duration as seconds
333 */
···432 )
433434 const onPressSelectMedia = useCallback(async () => {
435+ if (IS_NATIVE) {
436 const [photoAccess, videoAccess] = await Promise.all([
437 requestPhotoAccessIfNeeded(),
438 requestVideoAccessIfNeeded(),
···446 }
447 }
448449+ if (IS_NATIVE && Keyboard.isVisible()) {
450 Keyboard.dismiss()
451 }
452
+2-2
src/view/com/composer/labels/LabelsBtn.tsx
···9 type OtherSelfLabel,
10 type SelfLabel,
11} from '#/lib/moderation'
12-import {isWeb} from '#/platform/detection'
13import {atoms as a, useTheme, web} from '#/alf'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import * as Dialog from '#/components/Dialog'
···18import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
19import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
20import {Text} from '#/components/Typography'
02122export function LabelsBtn({
23 labels,
···218 label={_(msg`Done`)}
219 onPress={() => control.close()}
220 color="primary"
221- size={isWeb ? 'small' : 'large'}
222 variant="solid"
223 testID="confirmBtn">
224 <ButtonText>
···9 type OtherSelfLabel,
10 type SelfLabel,
11} from '#/lib/moderation'
012import {atoms as a, useTheme, web} from '#/alf'
13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
···17import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
18import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
19import {Text} from '#/components/Typography'
20+import {IS_WEB} from '#/env'
2122export function LabelsBtn({
23 labels,
···218 label={_(msg`Done`)}
219 onPress={() => control.close()}
220 color="primary"
221+ size={IS_WEB ? 'small' : 'large'}
222 variant="solid"
223 testID="confirmBtn">
224 <ButtonText>
+2-2
src/view/com/composer/photos/Gallery.tsx
···16import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
17import {type Dimensions} from '#/lib/media/types'
18import {colors, s} from '#/lib/styles'
19-import {isNative} from '#/platform/detection'
20import {type ComposerImage, cropImage} from '#/state/gallery'
21import {Text} from '#/view/com/util/text/Text'
22import {tokens, useTheme} from '#/alf'
23import * as Dialog from '#/components/Dialog'
24import {MediaInsetBorder} from '#/components/MediaInsetBorder'
025import {type PostAction} from '../state/composer'
26import {EditImageDialog} from './EditImageDialog'
27import {ImageAltTextDialog} from './ImageAltTextDialog'
···145 const editControl = Dialog.useDialogControl()
146147 const onImageEdit = () => {
148- if (isNative) {
149 cropImage(image).then(next => {
150 onChange(next)
151 })
···16import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
17import {type Dimensions} from '#/lib/media/types'
18import {colors, s} from '#/lib/styles'
019import {type ComposerImage, cropImage} from '#/state/gallery'
20import {Text} from '#/view/com/util/text/Text'
21import {tokens, useTheme} from '#/alf'
22import * as Dialog from '#/components/Dialog'
23import {MediaInsetBorder} from '#/components/MediaInsetBorder'
24+import {IS_NATIVE} from '#/env'
25import {type PostAction} from '../state/composer'
26import {EditImageDialog} from './EditImageDialog'
27import {ImageAltTextDialog} from './ImageAltTextDialog'
···145 const editControl = Dialog.useDialogControl()
146147 const onImageEdit = () => {
148+ if (IS_NATIVE) {
149 cropImage(image).then(next => {
150 onChange(next)
151 })
···89import {isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11-import {isNative} from '#/platform/detection'
12import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
13import {createPostgateRecord} from '#/state/queries/postgate/util'
14import {usePreferencesQuery} from '#/state/queries/preferences'
···25import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
26import * as Tooltip from '#/components/Tooltip'
27import {Text} from '#/components/Typography'
028import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
2930export function ThreadgateBtn({
···70 nudged: tooltipWasShown,
71 })
7273- if (isNative && Keyboard.isVisible()) {
74 Keyboard.dismiss()
75 }
76
···89import {isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
011import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
12import {createPostgateRecord} from '#/state/queries/postgate/util'
13import {usePreferencesQuery} from '#/state/queries/preferences'
···24import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
25import * as Tooltip from '#/components/Tooltip'
26import {Text} from '#/components/Typography'
27+import {IS_NATIVE} from '#/env'
28import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
2930export function ThreadgateBtn({
···70 nudged: tooltipWasShown,
71 })
7273+ if (IS_NATIVE && Keyboard.isVisible()) {
74 Keyboard.dismiss()
75 }
76
+10-6
src/view/com/composer/videos/SubtitleDialog.tsx
···6import {MAX_ALT_TEXT} from '#/lib/constants'
7import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
8import {LANGUAGES} from '#/locale/languages'
9-import {isWeb} from '#/platform/detection'
10import {useLanguagePrefs} from '#/state/preferences'
11import {atoms as a, useTheme, web} from '#/alf'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···17import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
18import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
19import {Text} from '#/components/Typography'
020import {SubtitleFilePicker} from './SubtitleFilePicker'
2122const MAX_NUM_CAPTIONS = 1
···37 return (
38 <View style={[a.flex_row, a.my_xs]}>
39 <Button
40- label={isWeb ? _(msg`Captions & alt text`) : _(msg`Alt text`)}
41 accessibilityHint={
42- isWeb
43 ? _(msg`Opens captions and alt text dialog`)
44 : _(msg`Opens alt text dialog`)
45 }
···52 }}>
53 <ButtonIcon icon={CCIcon} />
54 <ButtonText>
55- {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
000056 </ButtonText>
57 </Button>
58 <Dialog.Outer control={control}>
···134 </Text>
135 )}
136137- {isWeb && (
138 <>
139 <View
140 style={[
···182 <View style={web([a.flex_row, a.justify_end])}>
183 <Button
184 label={_(msg`Done`)}
185- size={isWeb ? 'small' : 'large'}
186 color="primary"
187 variant="solid"
188 onPress={() => {
···6import {MAX_ALT_TEXT} from '#/lib/constants'
7import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
8import {LANGUAGES} from '#/locale/languages'
09import {useLanguagePrefs} from '#/state/preferences'
10import {atoms as a, useTheme, web} from '#/alf'
11import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···16import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
17import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
18import {Text} from '#/components/Typography'
19+import {IS_WEB} from '#/env'
20import {SubtitleFilePicker} from './SubtitleFilePicker'
2122const MAX_NUM_CAPTIONS = 1
···37 return (
38 <View style={[a.flex_row, a.my_xs]}>
39 <Button
40+ label={IS_WEB ? _(msg`Captions & alt text`) : _(msg`Alt text`)}
41 accessibilityHint={
42+ IS_WEB
43 ? _(msg`Opens captions and alt text dialog`)
44 : _(msg`Opens alt text dialog`)
45 }
···52 }}>
53 <ButtonIcon icon={CCIcon} />
54 <ButtonText>
55+ {IS_WEB ? (
56+ <Trans>Captions & alt text</Trans>
57+ ) : (
58+ <Trans>Alt text</Trans>
59+ )}
60 </ButtonText>
61 </Button>
62 <Dialog.Outer control={control}>
···138 </Text>
139 )}
140141+ {IS_WEB && (
142 <>
143 <View
144 style={[
···186 <View style={web([a.flex_row, a.justify_end])}>
187 <Button
188 label={_(msg`Done`)}
189+ size={IS_WEB ? 'small' : 'large'}
190 color="primary"
191 variant="solid"
192 onPress={() => {
···4import {type ImagePickerAsset} from 'expo-image-picker'
56import {clamp} from '#/lib/numbers'
7-import {isWeb} from '#/platform/detection'
8import {atoms as a, useTheme} from '#/alf'
09import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
10import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
11···20}) {
21 const t = useTheme()
2223- if (isWeb) return null
2425 let aspectRatio = asset.width / asset.height
26
···4import {type ImagePickerAsset} from 'expo-image-picker'
56import {clamp} from '#/lib/numbers'
07import {atoms as a, useTheme} from '#/alf'
8+import {IS_WEB} from '#/env'
9import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
10import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
11···20}) {
21 const t = useTheme()
2223+ if (IS_WEB) return null
2425 let aspectRatio = asset.width / asset.height
26
+5-5
src/view/com/feeds/ComposerPrompt.tsx
···11} from '#/lib/hooks/usePermissions'
12import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
13import {logger} from '#/logger'
14-import {isNative} from '#/platform/detection'
15import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
16import {MAX_IMAGES} from '#/view/com/composer/state/composer'
17import {UserAvatar} from '#/view/com/util/UserAvatar'
···22import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
23import {SubtleHover} from '#/components/SubtleHover'
24import {Text} from '#/components/Typography'
02526export function ComposerPrompt() {
27 const {_} = useLingui()
···43 logger.metric('composerPrompt:gallery:press', {})
4445 // On web, open the composer with the gallery picker auto-opening
46- if (!isNative) {
47 openComposer({openGallery: true})
48 return
49 }
···105 return
106 }
107108- if (isNative && Keyboard.isVisible()) {
109 Keyboard.dismiss()
110 }
111···122 ]
123124 openComposer({
125- imageUris: isNative ? imageUris : undefined,
126 })
127 } catch (err: any) {
128 if (!String(err).toLowerCase().includes('cancel')) {
···189 <Trans>What's up?</Trans>
190 </Text>
191 <View style={[a.flex_row, a.gap_md]}>
192- {isNative && (
193 <Button
194 onPress={e => {
195 e.stopPropagation()
···11} from '#/lib/hooks/usePermissions'
12import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
13import {logger} from '#/logger'
014import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
15import {MAX_IMAGES} from '#/view/com/composer/state/composer'
16import {UserAvatar} from '#/view/com/util/UserAvatar'
···21import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
22import {SubtleHover} from '#/components/SubtleHover'
23import {Text} from '#/components/Typography'
24+import {IS_NATIVE} from '#/env'
2526export function ComposerPrompt() {
27 const {_} = useLingui()
···43 logger.metric('composerPrompt:gallery:press', {})
4445 // On web, open the composer with the gallery picker auto-opening
46+ if (!IS_NATIVE) {
47 openComposer({openGallery: true})
48 return
49 }
···105 return
106 }
107108+ if (IS_NATIVE && Keyboard.isVisible()) {
109 Keyboard.dismiss()
110 }
111···122 ]
123124 openComposer({
125+ imageUris: IS_NATIVE ? imageUris : undefined,
126 })
127 } catch (err: any) {
128 if (!String(err).toLowerCase().includes('cancel')) {
···189 <Trans>What's up?</Trans>
190 </Text>
191 <View style={[a.flex_row, a.gap_md]}>
192+ {IS_NATIVE && (
193 <Button
194 onPress={e => {
195 e.stopPropagation()
+4-4
src/view/com/feeds/FeedPage.tsx
···20import {type AllNavigatorParams} from '#/lib/routes/types'
21import {logEvent} from '#/lib/statsig/statsig'
22import {s} from '#/lib/styles'
23-import {isNative} from '#/platform/detection'
24import {listenSoftReset} from '#/state/events'
25import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
26import {useSetHomeBadge} from '#/state/home-badge'
···34import {useSession} from '#/state/session'
35import {useSetMinimalShellMode} from '#/state/shell'
36import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
037import {PostFeed} from '../posts/PostFeed'
38import {FAB} from '../util/fab/FAB'
39import {type ListMethods} from '../util/List'
···80 const feedIsVideoMode =
81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
83- return isNative && _isVideoFeed
84 }, [feedInfo])
8586 useEffect(() => {
···9192 const scrollToTop = useCallback(() => {
93 scrollElRef.current?.scrollToOffset({
94- animated: isNative,
95 offset: -headerOffset,
96 })
97 setMinimalShellMode(false)
···136 })
137 }, [scrollToTop, feed, queryClient])
138139- const shouldPrefetch = isNative && isPageAdjacent
140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
141 return (
142 <View
···20import {type AllNavigatorParams} from '#/lib/routes/types'
21import {logEvent} from '#/lib/statsig/statsig'
22import {s} from '#/lib/styles'
023import {listenSoftReset} from '#/state/events'
24import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
25import {useSetHomeBadge} from '#/state/home-badge'
···33import {useSession} from '#/state/session'
34import {useSetMinimalShellMode} from '#/state/shell'
35import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
36+import {IS_NATIVE} from '#/env'
37import {PostFeed} from '../posts/PostFeed'
38import {FAB} from '../util/fab/FAB'
39import {type ListMethods} from '../util/List'
···80 const feedIsVideoMode =
81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
83+ return IS_NATIVE && _isVideoFeed
84 }, [feedInfo])
8586 useEffect(() => {
···9192 const scrollToTop = useCallback(() => {
93 scrollElRef.current?.scrollToOffset({
94+ animated: IS_NATIVE,
95 offset: -headerOffset,
96 })
97 setMinimalShellMode(false)
···136 })
137 }, [scrollToTop, feed, queryClient])
138139+ const shouldPrefetch = IS_NATIVE && isPageAdjacent
140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
141 return (
142 <View
+3-3
src/view/com/feeds/MissingFeed.tsx
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
7-import {isNative, isWeb} from '#/platform/detection'
8import {useModerationOpts} from '#/state/preferences/moderation-opts'
9import {getFeedTypeFromUri} from '#/state/queries/feed'
10import {useProfileQuery} from '#/state/queries/profile'
···15import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
16import * as ProfileCard from '#/components/ProfileCard'
17import {Text} from '#/components/Typography'
01819export function MissingFeed({
20 style,
···83 a.italic,
84 ]}
85 numberOfLines={1}>
86- {isWeb ? (
87 <Trans>Click for information</Trans>
88 ) : (
89 <Trans>Tap for information</Trans>
···205 </>
206 )}
207 </View>
208- {isNative && (
209 <Button
210 label={_(msg`Close`)}
211 onPress={() => control.close()}
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
07import {useModerationOpts} from '#/state/preferences/moderation-opts'
8import {getFeedTypeFromUri} from '#/state/queries/feed'
9import {useProfileQuery} from '#/state/queries/profile'
···14import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
15import * as ProfileCard from '#/components/ProfileCard'
16import {Text} from '#/components/Typography'
17+import {IS_NATIVE, IS_WEB} from '#/env'
1819export function MissingFeed({
20 style,
···83 a.italic,
84 ]}
85 numberOfLines={1}>
86+ {IS_WEB ? (
87 <Trans>Click for information</Trans>
88 ) : (
89 <Trans>Tap for information</Trans>
···205 </>
206 )}
207 </View>
208+ {IS_NATIVE && (
209 <Button
210 label={_(msg`Close`)}
211 onPress={() => control.close()}
+4-4
src/view/com/feeds/ProfileFeedgens.tsx
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23-import {isIOS, isNative, isWeb} from '#/platform/detection'
24import {usePreferencesQuery} from '#/state/queries/preferences'
25import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
26import {useSession} from '#/state/session'
···33import * as FeedCard from '#/components/FeedCard'
34import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
35import {ListFooter} from '#/components/Lists'
03637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114- animated: isNative,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···194 return (
195 <View
196 style={[
197- (index !== 0 || isWeb) && a.border_t,
198 t.atoms.border_contrast_low,
199 a.px_lg,
200 a.py_lg,
···218 )
219220 useEffect(() => {
221- if (isIOS && enabled && scrollElRef.current) {
222 const nativeTag = findNodeHandle(scrollElRef.current)
223 setScrollViewTag(nativeTag)
224 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
023import {usePreferencesQuery} from '#/state/queries/preferences'
24import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
25import {useSession} from '#/state/session'
···32import * as FeedCard from '#/components/FeedCard'
33import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
34import {ListFooter} from '#/components/Lists'
35+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
3637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114+ animated: IS_NATIVE,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···194 return (
195 <View
196 style={[
197+ (index !== 0 || IS_WEB) && a.border_t,
198 t.atoms.border_contrast_low,
199 a.px_lg,
200 a.py_lg,
···218 )
219220 useEffect(() => {
221+ if (IS_IOS && enabled && scrollElRef.current) {
222 const nativeTag = findNodeHandle(scrollElRef.current)
223 setScrollViewTag(nativeTag)
224 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23-import {isIOS, isNative, isWeb} from '#/platform/detection'
24import {usePreferencesQuery} from '#/state/queries/preferences'
25import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
26import {useSession} from '#/state/session'
···33import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
34import * as ListCard from '#/components/ListCard'
35import {ListFooter} from '#/components/Lists'
03637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114- animated: isNative,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···193 return (
194 <View
195 style={[
196- (index !== 0 || isWeb) && a.border_t,
197 t.atoms.border_contrast_low,
198 a.px_lg,
199 a.py_lg,
···217 )
218219 useEffect(() => {
220- if (isIOS && enabled && scrollElRef.current) {
221 const nativeTag = findNodeHandle(scrollElRef.current)
222 setScrollViewTag(nativeTag)
223 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
023import {usePreferencesQuery} from '#/state/queries/preferences'
24import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
25import {useSession} from '#/state/session'
···32import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
33import * as ListCard from '#/components/ListCard'
34import {ListFooter} from '#/components/Lists'
35+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
3637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114+ animated: IS_NATIVE,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···193 return (
194 <View
195 style={[
196+ (index !== 0 || IS_WEB) && a.border_t,
197 t.atoms.border_contrast_low,
198 a.px_lg,
199 a.py_lg,
···217 )
218219 useEffect(() => {
220+ if (IS_IOS && enabled && scrollElRef.current) {
221 const nativeTag = findNodeHandle(scrollElRef.current)
222 setScrollViewTag(nativeTag)
223 }
+3-3
src/view/com/modals/DeleteAccount.tsx
···16import {cleanError} from '#/lib/strings/errors'
17import {colors, gradients, s} from '#/lib/styles'
18import {useTheme} from '#/lib/ThemeContext'
19-import {isAndroid, isWeb} from '#/platform/detection'
20import {useModalControls} from '#/state/modals'
21import {useAgent, useSession, useSessionApi} from '#/state/session'
22import {atoms as a, useTheme as useNewTheme} from '#/alf'
23import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
24import {Text as NewText} from '#/components/Typography'
025import {resetToTab} from '../../../Navigation'
26import {ErrorMessage} from '../util/error/ErrorMessage'
27import {Text} from '../util/text/Text'
28import * as Toast from '../util/Toast'
29import {ScrollView, TextInput} from './util'
3031-export const snapPoints = isAndroid ? ['90%'] : ['55%']
3233export function Component({}: {}) {
34 const pal = usePalette('default')
···173 </>
174 )}
175176- <View style={[!isWeb && a.px_xl]}>
177 <View
178 style={[
179 a.w_full,
···16import {cleanError} from '#/lib/strings/errors'
17import {colors, gradients, s} from '#/lib/styles'
18import {useTheme} from '#/lib/ThemeContext'
019import {useModalControls} from '#/state/modals'
20import {useAgent, useSession, useSessionApi} from '#/state/session'
21import {atoms as a, useTheme as useNewTheme} from '#/alf'
22import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
23import {Text as NewText} from '#/components/Typography'
24+import {IS_ANDROID, IS_WEB} from '#/env'
25import {resetToTab} from '../../../Navigation'
26import {ErrorMessage} from '../util/error/ErrorMessage'
27import {Text} from '../util/text/Text'
28import * as Toast from '../util/Toast'
29import {ScrollView, TextInput} from './util'
3031+export const snapPoints = IS_ANDROID ? ['90%'] : ['55%']
3233export function Component({}: {}) {
34 const pal = usePalette('default')
···173 </>
174 )}
175176+ <View style={[!IS_WEB && a.px_xl]}>
177 <View
178 style={[
179 a.w_full,
···1import React, {useContext} from 'react'
2import {type SharedValue} from 'react-native-reanimated'
34-import {isNative} from '#/platform/detection'
56export const PagerHeaderContext = React.createContext<{
7 scrollY: SharedValue<number>
···3738export function usePagerHeaderContext() {
39 const ctx = useContext(PagerHeaderContext)
40- if (isNative) {
41 if (!ctx) {
42 throw new Error(
43 'usePagerHeaderContext must be used within a HeaderProvider',
···1import React, {useContext} from 'react'
2import {type SharedValue} from 'react-native-reanimated'
34+import {IS_NATIVE} from '#/env'
56export const PagerHeaderContext = React.createContext<{
7 scrollY: SharedValue<number>
···3738export function usePagerHeaderContext() {
39 const ctx = useContext(PagerHeaderContext)
40+ if (IS_NATIVE) {
41 if (!ctx) {
42 throw new Error(
43 'usePagerHeaderContext must be used within a HeaderProvider',
+3-3
src/view/com/pager/PagerWithHeader.tsx
···1819import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
20import {ScrollProvider} from '#/lib/ScrollContext'
21-import {isIOS} from '#/platform/detection'
22import {
23 Pager,
24 type PagerRef,
25 type RenderTabBarFnProps,
26} from '#/view/com/pager/Pager'
27import {useTheme} from '#/alf'
028import {type ListMethods} from '../util/List'
29import {PagerHeaderProvider} from './PagerHeaderContext'
30import {TabBar} from './TabBar'
···273 const headerRef = useRef(null)
274 return (
275 <Animated.View
276- pointerEvents={isIOS ? 'auto' : 'box-none'}
277 style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
278 <View
279 ref={headerRef}
280- pointerEvents={isIOS ? 'auto' : 'box-none'}
281 collapsable={false}>
282 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})}
283 {
···1819import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
20import {ScrollProvider} from '#/lib/ScrollContext'
021import {
22 Pager,
23 type PagerRef,
24 type RenderTabBarFnProps,
25} from '#/view/com/pager/Pager'
26import {useTheme} from '#/alf'
27+import {IS_IOS} from '#/env'
28import {type ListMethods} from '../util/List'
29import {PagerHeaderProvider} from './PagerHeaderContext'
30import {TabBar} from './TabBar'
···273 const headerRef = useRef(null)
274 return (
275 <Animated.View
276+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
277 style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
278 <View
279 ref={headerRef}
280+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
281 collapsable={false}>
282 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})}
283 {
+2-2
src/view/com/posts/CustomFeedEmptyState.tsx
···13import {type NavigationProp} from '#/lib/routes/types'
14import {s} from '#/lib/styles'
15import {logger} from '#/logger'
16-import {isWeb} from '#/platform/detection'
17import {useFeedFeedbackContext} from '#/state/feed-feedback'
18import {useSession} from '#/state/session'
019import {Button} from '../util/forms/Button'
20import {Text} from '../util/text/Text'
21···44 const navigation = useNavigation<NavigationProp>()
4546 const onPressFindAccounts = React.useCallback(() => {
47- if (isWeb) {
48 navigation.navigate('Search', {})
49 } else {
50 navigation.navigate('SearchTab')
···13import {type NavigationProp} from '#/lib/routes/types'
14import {s} from '#/lib/styles'
15import {logger} from '#/logger'
016import {useFeedFeedbackContext} from '#/state/feed-feedback'
17import {useSession} from '#/state/session'
18+import {IS_WEB} from '#/env'
19import {Button} from '../util/forms/Button'
20import {Text} from '../util/text/Text'
21···44 const navigation = useNavigation<NavigationProp>()
4546 const onPressFindAccounts = React.useCallback(() => {
47+ if (IS_WEB) {
48 navigation.navigate('Search', {})
49 } else {
50 navigation.navigate('SearchTab')
+2-2
src/view/com/posts/FollowingEmptyState.tsx
···11import {MagnifyingGlassIcon} from '#/lib/icons'
12import {type NavigationProp} from '#/lib/routes/types'
13import {s} from '#/lib/styles'
14-import {isWeb} from '#/platform/detection'
15import {Button} from '../util/forms/Button'
16import {Text} from '../util/text/Text'
17···21 const navigation = useNavigation<NavigationProp>()
2223 const onPressFindAccounts = React.useCallback(() => {
24- if (isWeb) {
25 navigation.navigate('Search', {})
26 } else {
27 navigation.navigate('SearchTab')
···11import {MagnifyingGlassIcon} from '#/lib/icons'
12import {type NavigationProp} from '#/lib/routes/types'
13import {s} from '#/lib/styles'
14+import {IS_WEB} from '#/env'
15import {Button} from '../util/forms/Button'
16import {Text} from '../util/text/Text'
17···21 const navigation = useNavigation<NavigationProp>()
2223 const onPressFindAccounts = React.useCallback(() => {
24+ if (IS_WEB) {
25 navigation.navigate('Search', {})
26 } else {
27 navigation.navigate('SearchTab')
+2-2
src/view/com/posts/FollowingEndOfFeed.tsx
···10import {usePalette} from '#/lib/hooks/usePalette'
11import {type NavigationProp} from '#/lib/routes/types'
12import {s} from '#/lib/styles'
13-import {isWeb} from '#/platform/detection'
14import {Button} from '../util/forms/Button'
15import {Text} from '../util/text/Text'
16···20 const navigation = useNavigation<NavigationProp>()
2122 const onPressFindAccounts = React.useCallback(() => {
23- if (isWeb) {
24 navigation.navigate('Search', {})
25 } else {
26 navigation.navigate('SearchTab')
···10import {usePalette} from '#/lib/hooks/usePalette'
11import {type NavigationProp} from '#/lib/routes/types'
12import {s} from '#/lib/styles'
13+import {IS_WEB} from '#/env'
14import {Button} from '../util/forms/Button'
15import {Text} from '../util/text/Text'
16···20 const navigation = useNavigation<NavigationProp>()
2122 const onPressFindAccounts = React.useCallback(() => {
23+ if (IS_WEB) {
24 navigation.navigate('Search', {})
25 } else {
26 navigation.navigate('SearchTab')
+4-4
src/view/com/posts/PostFeed.tsx
···34import {logEvent, useGate} from '#/lib/statsig/statsig'
35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
37-import {isIOS, isNative, isWeb} from '#/platform/detection'
38import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
39import {listenPostCreated} from '#/state/events'
40import {useFeedFeedbackContext} from '#/state/feed-feedback'
···70} from '#/components/feeds/PostFeedVideoGridRow'
71import {TrendingInterstitial} from '#/components/interstitials/Trending'
72import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
073import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
74import {ComposerPrompt} from '../feeds/ComposerPrompt'
75import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
···243 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|')
244 const {gtMobile} = useBreakpoints()
245 const {rightNavVisible} = useLayoutBreakpoints()
246- const areVideoFeedsEnabled = isNative
247248 const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState(
249 () => new Set<string>(),
···873 * reach the end, so that content isn't cut off by the bottom of the
874 * screen.
875 */
876- const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2)
877878 return isFetchingNextPage ? (
879 <View style={[styles.feedFooter]}>
···1024 }
1025 initialNumToRender={initialNumToRenderOverride ?? initialNumToRender}
1026 windowSize={9}
1027- maxToRenderPerBatch={isIOS ? 5 : 1}
1028 updateCellsBatchingPeriod={40}
1029 onItemSeen={onItemSeen}
1030 />
···34import {logEvent, useGate} from '#/lib/statsig/statsig'
35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
037import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
38import {listenPostCreated} from '#/state/events'
39import {useFeedFeedbackContext} from '#/state/feed-feedback'
···69} from '#/components/feeds/PostFeedVideoGridRow'
70import {TrendingInterstitial} from '#/components/interstitials/Trending'
71import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
72+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
73import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
74import {ComposerPrompt} from '../feeds/ComposerPrompt'
75import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
···243 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|')
244 const {gtMobile} = useBreakpoints()
245 const {rightNavVisible} = useLayoutBreakpoints()
246+ const areVideoFeedsEnabled = IS_NATIVE
247248 const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState(
249 () => new Set<string>(),
···873 * reach the end, so that content isn't cut off by the bottom of the
874 * screen.
875 */
876+ const offset = Math.max(headerOffset, 32) * (IS_WEB ? 1 : 2)
877878 return isFetchingNextPage ? (
879 <View style={[styles.feedFooter]}>
···1024 }
1025 initialNumToRender={initialNumToRenderOverride ?? initialNumToRender}
1026 windowSize={9}
1027+ maxToRenderPerBatch={IS_IOS ? 5 : 1}
1028 updateCellsBatchingPeriod={40}
1029 onItemSeen={onItemSeen}
1030 />
+2-2
src/view/com/profile/ProfileFollows.tsx
···8import {type NavigationProp} from '#/lib/routes/types'
9import {cleanError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11-import {isWeb} from '#/platform/detection'
12import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
13import {useResolveDidQuery} from '#/state/queries/resolve-uri'
14import {useSession} from '#/state/session'
15import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX'
16import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
17import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
018import {List} from '../util/List'
19import {ProfileCardWithFollowBtn} from './ProfileCard'
20···49 const navigation = useNavigation<NavigationProp>()
5051 const onPressFindAccounts = React.useCallback(() => {
52- if (isWeb) {
53 navigation.navigate('Search', {})
54 } else {
55 navigation.navigate('SearchTab')
···8import {type NavigationProp} from '#/lib/routes/types'
9import {cleanError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
011import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
12import {useResolveDidQuery} from '#/state/queries/resolve-uri'
13import {useSession} from '#/state/session'
14import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX'
15import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
16import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
17+import {IS_WEB} from '#/env'
18import {List} from '../util/List'
19import {ProfileCardWithFollowBtn} from './ProfileCard'
20···49 const navigation = useNavigation<NavigationProp>()
5051 const onPressFindAccounts = React.useCallback(() => {
52+ if (IS_WEB) {
53 navigation.navigate('Search', {})
54 } else {
55 navigation.navigate('SearchTab')
+7-5
src/view/com/profile/ProfileMenu.tsx
···12import {shareText, shareUrl} from '#/lib/sharing'
13import {toShareUrl} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
15-import {isWeb} from '#/platform/detection'
16import {type Shadow} from '#/state/cache/types'
17import {useModalControls} from '#/state/modals'
18import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
···61import {useFullVerificationState} from '#/components/verification'
62import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
63import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
064import {Dot} from '#/features/nuxs/components/Dot'
65import {Gradient} from '#/features/nuxs/components/Gradient'
66import {useDevMode} from '#/storage/hooks/dev-mode'
···266 <Menu.Item
267 testID="profileHeaderDropdownShareBtn"
268 label={
269- isWeb ? _(msg`Copy link to profile`) : _(msg`Share via...`)
270 }
271 onPress={() => {
272 if (showLoggedOutWarning) {
···276 }
277 }}>
278 <Menu.ItemText>
279- {isWeb ? (
280 <Trans>Copy link to profile</Trans>
281 ) : (
282 <Trans>Share via...</Trans>
283 )}
284 </Menu.ItemText>
285- <Menu.ItemIcon icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} />
00286 </Menu.Item>
287 <Menu.Item
288 testID="profileHeaderDropdownSearchBtn"
···382 a.flex_0,
383 {
384 color: t.palette.primary_500,
385- right: isWeb ? -8 : -4,
386 },
387 ]}>
388 <Trans>New</Trans>
···12import {shareText, shareUrl} from '#/lib/sharing'
13import {toShareUrl} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
015import {type Shadow} from '#/state/cache/types'
16import {useModalControls} from '#/state/modals'
17import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
···60import {useFullVerificationState} from '#/components/verification'
61import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
62import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
63+import {IS_WEB} from '#/env'
64import {Dot} from '#/features/nuxs/components/Dot'
65import {Gradient} from '#/features/nuxs/components/Gradient'
66import {useDevMode} from '#/storage/hooks/dev-mode'
···266 <Menu.Item
267 testID="profileHeaderDropdownShareBtn"
268 label={
269+ IS_WEB ? _(msg`Copy link to profile`) : _(msg`Share via...`)
270 }
271 onPress={() => {
272 if (showLoggedOutWarning) {
···276 }
277 }}>
278 <Menu.ItemText>
279+ {IS_WEB ? (
280 <Trans>Copy link to profile</Trans>
281 ) : (
282 <Trans>Share via...</Trans>
283 )}
284 </Menu.ItemText>
285+ <Menu.ItemIcon
286+ icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon}
287+ />
288 </Menu.Item>
289 <Menu.Item
290 testID="profileHeaderDropdownSearchBtn"
···384 a.flex_0,
385 {
386 color: t.palette.primary_500,
387+ right: IS_WEB ? -8 : -4,
388 },
389 ]}>
390 <Trans>New</Trans>
+4-4
src/view/com/util/Link.tsx
···25 linkRequiresWarning,
26} from '#/lib/strings/url-helpers'
27import {type TypographyVariant} from '#/lib/ThemeContext'
28-import {isAndroid, isWeb} from '#/platform/detection'
29import {emitSoftReset} from '#/state/events'
30import {useModalControls} from '#/state/modals'
31import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
32import {useTheme} from '#/alf'
33import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
034import {router} from '../../../routes'
35import {PressableWithHover} from './PressableWithHover'
36import {Text} from './text/Text'
···130 android_ripple={{
131 color: t.atoms.bg_contrast_25.backgroundColor,
132 }}
133- unstable_pressDelay={isAndroid ? 90 : undefined}>
134 {/* @ts-ignore web only -prf */}
135 <View style={style} href={anchorHref}>
136 {children ? children : <Text>{title || 'link'}</Text>}
···219 })
220 }
221 if (
222- isWeb &&
223 href !== '#' &&
224 e != null &&
225 isModifiedEvent(e as React.MouseEvent)
···323 onBeforePress,
324 ...props
325}: TextLinkOnWebOnlyProps) {
326- if (isWeb) {
327 return (
328 <TextLink
329 testID={testID}
···25 linkRequiresWarning,
26} from '#/lib/strings/url-helpers'
27import {type TypographyVariant} from '#/lib/ThemeContext'
028import {emitSoftReset} from '#/state/events'
29import {useModalControls} from '#/state/modals'
30import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
31import {useTheme} from '#/alf'
32import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
33+import {IS_ANDROID, IS_WEB} from '#/env'
34import {router} from '../../../routes'
35import {PressableWithHover} from './PressableWithHover'
36import {Text} from './text/Text'
···130 android_ripple={{
131 color: t.atoms.bg_contrast_25.backgroundColor,
132 }}
133+ unstable_pressDelay={IS_ANDROID ? 90 : undefined}>
134 {/* @ts-ignore web only -prf */}
135 <View style={style} href={anchorHref}>
136 {children ? children : <Text>{title || 'link'}</Text>}
···219 })
220 }
221 if (
222+ IS_WEB &&
223 href !== '#' &&
224 e != null &&
225 isModifiedEvent(e as React.MouseEvent)
···323 onBeforePress,
324 ...props
325}: TextLinkOnWebOnlyProps) {
326+ if (IS_WEB) {
327 return (
328 <TextLink
329 testID={testID}
+3-3
src/view/com/util/List.tsx
···11import {useDedupe} from '#/lib/hooks/useDedupe'
12import {useScrollHandlers} from '#/lib/ScrollContext'
13import {addStyle} from '#/lib/styles'
14-import {isIOS} from '#/platform/detection'
15import {useLightbox} from '#/state/lightbox'
16import {useTheme} from '#/alf'
017import {FlatList_INTERNAL} from './Views'
1819export type ListMethods = FlatList_INTERNAL
···94 }
95 }
9697- if (isIOS) {
98 runOnJS(dedupe)(updateActiveVideoViewAsync)
99 }
100 },
···184185// We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only
186// removing it saves us a re-render on Android
187-const useAllowScrollToTop = isIOS ? useAllowScrollToTopIOS : () => undefined
188function useAllowScrollToTopIOS() {
189 const {activeLightbox} = useLightbox()
190 return !activeLightbox
···11import {useDedupe} from '#/lib/hooks/useDedupe'
12import {useScrollHandlers} from '#/lib/ScrollContext'
13import {addStyle} from '#/lib/styles'
014import {useLightbox} from '#/state/lightbox'
15import {useTheme} from '#/alf'
16+import {IS_IOS} from '#/env'
17import {FlatList_INTERNAL} from './Views'
1819export type ListMethods = FlatList_INTERNAL
···94 }
95 }
9697+ if (IS_IOS) {
98 runOnJS(dedupe)(updateActiveVideoViewAsync)
99 }
100 },
···184185// We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only
186// removing it saves us a re-render on Android
187+const useAllowScrollToTop = IS_IOS ? useAllowScrollToTopIOS : () => undefined
188function useAllowScrollToTopIOS() {
189 const {activeLightbox} = useLightbox()
190 return !activeLightbox
···12import {sanitizeDisplayName} from '#/lib/strings/display-names'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {niceDate} from '#/lib/strings/time'
15-import {isAndroid} from '#/platform/detection'
16import {useProfileShadow} from '#/state/cache/profile-shadow'
17import {precacheProfile} from '#/state/queries/profile'
18import {atoms as a, platform, useTheme, web} from '#/alf'
···21import {Text} from '#/components/Typography'
22import {useSimpleVerificationState} from '#/components/verification'
23import {VerificationCheck} from '#/components/verification/VerificationCheck'
024import {TimeElapsed} from './TimeElapsed'
25import {PreviewableUserAvatar} from './UserAvatar'
26···152 a.pl_xs,
153 a.text_md,
154 a.leading_tight,
155- isAndroid && a.flex_grow,
156 a.text_right,
157 t.atoms.text_contrast_medium,
158 web({
159 whiteSpace: 'nowrap',
160 }),
161 ]}>
162- {!isAndroid && (
163 <Text
164 style={[
165 a.text_md,
···12import {sanitizeDisplayName} from '#/lib/strings/display-names'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {niceDate} from '#/lib/strings/time'
015import {useProfileShadow} from '#/state/cache/profile-shadow'
16import {precacheProfile} from '#/state/queries/profile'
17import {atoms as a, platform, useTheme, web} from '#/alf'
···20import {Text} from '#/components/Typography'
21import {useSimpleVerificationState} from '#/components/verification'
22import {VerificationCheck} from '#/components/verification/VerificationCheck'
23+import {IS_ANDROID} from '#/env'
24import {TimeElapsed} from './TimeElapsed'
25import {PreviewableUserAvatar} from './UserAvatar'
26···152 a.pl_xs,
153 a.text_md,
154 a.leading_tight,
155+ IS_ANDROID && a.flex_grow,
156 a.text_right,
157 t.atoms.text_contrast_medium,
158 web({
159 whiteSpace: 'nowrap',
160 }),
161 ]}>
162+ {!IS_ANDROID && (
163 <Text
164 style={[
165 a.text_md,
+7-7
src/view/com/util/UserAvatar.tsx
···30import {isCancelledError} from '#/lib/strings/errors'
31import {sanitizeHandle} from '#/lib/strings/handles'
32import {logger} from '#/logger'
33-import {isAndroid, isNative, isWeb} from '#/platform/detection'
34import {
35 type ComposerImage,
36 compressImage,
···54import {MediaInsetBorder} from '#/components/MediaInsetBorder'
55import * as Menu from '#/components/Menu'
56import {ProfileHoverCard} from '#/components/ProfileHoverCard'
057import type * as bsky from '#/types/bsky'
5859export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
···88 onBeforePress?: () => void
89}
9091-const BLUR_AMOUNT = isWeb ? 5 : 100
9293let DefaultAvatar = ({
94 type,
···286 }, [size, style])
287288 return avatar &&
289- !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
290 <View style={containerStyle}>
291 {usePlainRNImage ? (
292 <RNImage
···394 }
395396 try {
397- if (isNative) {
398 onSelectNewAvatar(
399 await compressIfNeeded(
400 await openCropper({
···464 </Menu.Trigger>
465 <Menu.Outer showCancel>
466 <Menu.Group>
467- {isNative && (
468 <Menu.Item
469 testID="changeAvatarCameraBtn"
470 label={_(msg`Upload from Camera`)}
···481 label={_(msg`Upload from Library`)}
482 onPress={onOpenLibrary}>
483 <Menu.ItemText>
484- {isNative ? (
485 <Trans>Upload from Library</Trans>
486 ) : (
487 <Trans>Upload from Files</Trans>
···571 <ProfileHoverCard did={profile.did} disable={disableHoverCard}>
572 {disableNavigation ? (
573 avatarEl
574- ) : status.isActive && (isNative || isTouchDevice) ? (
575 <>
576 <Button
577 label={_(
···30import {isCancelledError} from '#/lib/strings/errors'
31import {sanitizeHandle} from '#/lib/strings/handles'
32import {logger} from '#/logger'
033import {
34 type ComposerImage,
35 compressImage,
···53import {MediaInsetBorder} from '#/components/MediaInsetBorder'
54import * as Menu from '#/components/Menu'
55import {ProfileHoverCard} from '#/components/ProfileHoverCard'
56+import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env'
57import type * as bsky from '#/types/bsky'
5859export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler'
···88 onBeforePress?: () => void
89}
9091+const BLUR_AMOUNT = IS_WEB ? 5 : 100
9293let DefaultAvatar = ({
94 type,
···286 }, [size, style])
287288 return avatar &&
289+ !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? (
290 <View style={containerStyle}>
291 {usePlainRNImage ? (
292 <RNImage
···394 }
395396 try {
397+ if (IS_NATIVE) {
398 onSelectNewAvatar(
399 await compressIfNeeded(
400 await openCropper({
···464 </Menu.Trigger>
465 <Menu.Outer showCancel>
466 <Menu.Group>
467+ {IS_NATIVE && (
468 <Menu.Item
469 testID="changeAvatarCameraBtn"
470 label={_(msg`Upload from Camera`)}
···481 label={_(msg`Upload from Library`)}
482 onPress={onOpenLibrary}>
483 <Menu.ItemText>
484+ {IS_NATIVE ? (
485 <Trans>Upload from Library</Trans>
486 ) : (
487 <Trans>Upload from Files</Trans>
···571 <ProfileHoverCard did={profile.did} disable={disableHoverCard}>
572 {disableNavigation ? (
573 avatarEl
574+ ) : status.isActive && (IS_NATIVE || isTouchDevice) ? (
575 <>
576 <Button
577 label={_(
+5-5
src/view/com/util/UserBanner.tsx
···14import {type PickerImage} from '#/lib/media/picker.shared'
15import {isCancelledError} from '#/lib/strings/errors'
16import {logger} from '#/logger'
17-import {isAndroid, isNative} from '#/platform/detection'
18import {
19 type ComposerImage,
20 compressImage,
···32import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
33import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
34import * as Menu from '#/components/Menu'
03536export function UserBanner({
37 type,
···75 }
7677 try {
78- if (isNative) {
79 onSelectNewBanner?.(
80 await compressIfNeeded(
81 await openCropper({
···153 </Menu.Trigger>
154 <Menu.Outer showCancel>
155 <Menu.Group>
156- {isNative && (
157 <Menu.Item
158 testID="changeBannerCameraBtn"
159 label={_(msg`Upload from Camera`)}
···170 label={_(msg`Upload from Library`)}
171 onPress={onOpenLibrary}>
172 <Menu.ItemText>
173- {isNative ? (
174 <Trans>Upload from Library</Trans>
175 ) : (
176 <Trans>Upload from Files</Trans>
···207 />
208 </>
209 ) : banner &&
210- !((moderation?.blur && isAndroid) /* android crashes with blur */) ? (
211 <Image
212 testID="userBannerImage"
213 style={[styles.bannerImage, t.atoms.bg_contrast_25]}
···14import {type PickerImage} from '#/lib/media/picker.shared'
15import {isCancelledError} from '#/lib/strings/errors'
16import {logger} from '#/logger'
017import {
18 type ComposerImage,
19 compressImage,
···31import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
32import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
33import * as Menu from '#/components/Menu'
34+import {IS_ANDROID, IS_NATIVE} from '#/env'
3536export function UserBanner({
37 type,
···75 }
7677 try {
78+ if (IS_NATIVE) {
79 onSelectNewBanner?.(
80 await compressIfNeeded(
81 await openCropper({
···153 </Menu.Trigger>
154 <Menu.Outer showCancel>
155 <Menu.Group>
156+ {IS_NATIVE && (
157 <Menu.Item
158 testID="changeBannerCameraBtn"
159 label={_(msg`Upload from Camera`)}
···170 label={_(msg`Upload from Library`)}
171 onPress={onOpenLibrary}>
172 <Menu.ItemText>
173+ {IS_NATIVE ? (
174 <Trans>Upload from Library</Trans>
175 ) : (
176 <Trans>Upload from Files</Trans>
···207 />
208 </>
209 ) : banner &&
210+ !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? (
211 <Image
212 testID="userBannerImage"
213 style={[styles.bannerImage, t.atoms.bg_contrast_25]}
+2-2
src/view/com/util/ViewSelector.tsx
···13import {usePalette} from '#/lib/hooks/usePalette'
14import {clamp} from '#/lib/numbers'
15import {colors, s} from '#/lib/styles'
16-import {isAndroid} from '#/platform/detection'
17import {Text} from './text/Text'
18import {FlatList_INTERNAL} from './Views'
19···120 renderItem={renderItemInternal}
121 ListFooterComponent={ListFooterComponent}
122 // NOTE sticky header disabled on android due to major performance issues -prf
123- stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
124 onScroll={onScroll}
125 onEndReached={onEndReached}
126 refreshControl={
···13import {usePalette} from '#/lib/hooks/usePalette'
14import {clamp} from '#/lib/numbers'
15import {colors, s} from '#/lib/styles'
16+import {IS_ANDROID} from '#/env'
17import {Text} from './text/Text'
18import {FlatList_INTERNAL} from './Views'
19···120 renderItem={renderItemInternal}
121 ListFooterComponent={ListFooterComponent}
122 // NOTE sticky header disabled on android due to major performance issues -prf
123+ stickyHeaderIndices={IS_ANDROID ? undefined : STICKY_HEADER_INDICES}
124 onScroll={onScroll}
125 onEndReached={onEndReached}
126 refreshControl={
+2-2
src/view/com/util/fab/FABInner.tsx
···12import {useHaptics} from '#/lib/haptics'
13import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
14import {clamp} from '#/lib/numbers'
15-import {isWeb} from '#/platform/detection'
16import {ios, useBreakpoints, useTheme} from '#/alf'
17import {atoms as a} from '#/alf'
01819export interface FABProps extends ComponentProps<typeof Pressable> {
20 testID?: string
···84 },
85 outer: {
86 // @ts-ignore web-only
87- position: isWeb ? 'fixed' : 'absolute',
88 zIndex: 1,
89 cursor: 'pointer',
90 },
···12import {useHaptics} from '#/lib/haptics'
13import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform'
14import {clamp} from '#/lib/numbers'
015import {ios, useBreakpoints, useTheme} from '#/alf'
16import {atoms as a} from '#/alf'
17+import {IS_WEB} from '#/env'
1819export interface FABProps extends ComponentProps<typeof Pressable> {
20 testID?: string
···84 },
85 outer: {
86 // @ts-ignore web-only
87+ position: IS_WEB ? 'fixed' : 'absolute',
88 zIndex: 1,
89 cursor: 'pointer',
90 },
+2-2
src/view/com/util/layouts/LoggedOutLayout.tsx
···5import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
6import {usePalette} from '#/lib/hooks/usePalette'
7import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
8-import {isWeb} from '#/platform/detection'
9import {atoms as a} from '#/alf'
010import {Text} from '../text/Text'
1112export const LoggedOutLayout = ({
···79 contentContainerStyle={styles.scrollViewContentContainer}
80 keyboardShouldPersistTaps="handled"
81 keyboardDismissMode="on-drag">
82- <View style={[styles.contentWrapper, isWeb && a.my_auto]}>
83 {children}
84 </View>
85 </ScrollView>
···5import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
6import {usePalette} from '#/lib/hooks/usePalette'
7import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
08import {atoms as a} from '#/alf'
9+import {IS_WEB} from '#/env'
10import {Text} from '../text/Text'
1112export const LoggedOutLayout = ({
···79 contentContainerStyle={styles.scrollViewContentContainer}
80 keyboardShouldPersistTaps="handled"
81 keyboardDismissMode="on-drag">
82+ <View style={[styles.contentWrapper, IS_WEB && a.my_auto]}>
83 {children}
84 </View>
85 </ScrollView>
+3-3
src/view/com/util/text/Text.tsx
···5import {lh, s} from '#/lib/styles'
6import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
7import {logger} from '#/logger'
8-import {isIOS, isWeb} from '#/platform/detection'
9import {applyFonts, useAlf} from '#/alf'
10import {
11 childHasEmoji,
12 renderChildrenWithEmoji,
13 type StringChild,
14} from '#/alf/typography'
01516export type CustomTextProps = Omit<TextProps, 'children'> & {
17 type?: TypographyVariant
···81 }
8283 return {
84- uiTextView: selectable && isIOS,
85 selectable,
86 style: flattened,
87- dataSet: isWeb
88 ? Object.assign({tooltip: title}, dataSet || {})
89 : undefined,
90 ...props,
···5import {lh, s} from '#/lib/styles'
6import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
7import {logger} from '#/logger'
08import {applyFonts, useAlf} from '#/alf'
9import {
10 childHasEmoji,
11 renderChildrenWithEmoji,
12 type StringChild,
13} from '#/alf/typography'
14+import {IS_IOS, IS_WEB} from '#/env'
1516export type CustomTextProps = Omit<TextProps, 'children'> & {
17 type?: TypographyVariant
···81 }
8283 return {
84+ uiTextView: selectable && IS_IOS,
85 selectable,
86 style: flattened,
87+ dataSet: IS_WEB
88 ? Object.assign({tooltip: title}, dataSet || {})
89 : undefined,
90 ...props,
+4-4
src/view/screens/Feeds.tsx
···16} from '#/lib/routes/types'
17import {cleanError} from '#/lib/strings/errors'
18import {s} from '#/lib/styles'
19-import {isNative, isWeb} from '#/platform/detection'
20import {
21 type SavedFeedItem,
22 useGetPopularFeedsQuery,
···46import * as Layout from '#/components/Layout'
47import {Link} from '#/components/Link'
48import * as ListCard from '#/components/ListCard'
04950type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
51···385 const onChangeSearchFocus = React.useCallback(
386 (focus: boolean) => {
387 if (focus && searchBarIndex > -1) {
388- if (isNative) {
389 // scrollToIndex scrolls the exact right amount, so use if available
390 listRef.current?.scrollToIndex({
391 index: searchBarIndex,
···681 return (
682 <View
683 style={
684- isWeb
685 ? [
686 a.flex_row,
687 a.px_md,
···717 return (
718 <View
719 style={
720- isWeb
721 ? [a.flex_row, a.px_md, a.pt_lg, a.pb_lg, a.gap_md]
722 : [{flexDirection: 'row-reverse'}, a.p_lg, a.gap_md]
723 }>
···16} from '#/lib/routes/types'
17import {cleanError} from '#/lib/strings/errors'
18import {s} from '#/lib/styles'
019import {
20 type SavedFeedItem,
21 useGetPopularFeedsQuery,
···45import * as Layout from '#/components/Layout'
46import {Link} from '#/components/Link'
47import * as ListCard from '#/components/ListCard'
48+import {IS_NATIVE, IS_WEB} from '#/env'
4950type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
51···385 const onChangeSearchFocus = React.useCallback(
386 (focus: boolean) => {
387 if (focus && searchBarIndex > -1) {
388+ if (IS_NATIVE) {
389 // scrollToIndex scrolls the exact right amount, so use if available
390 listRef.current?.scrollToIndex({
391 index: searchBarIndex,
···681 return (
682 <View
683 style={
684+ IS_WEB
685 ? [
686 a.flex_row,
687 a.px_md,
···717 return (
718 <View
719 style={
720+ IS_WEB
721 ? [a.flex_row, a.px_md, a.pt_lg, a.pb_lg, a.gap_md]
722 : [{flexDirection: 'row-reverse'}, a.p_lg, a.gap_md]
723 }>
+2-2
src/view/screens/Home.tsx
···12 type NativeStackScreenProps,
13} from '#/lib/routes/types'
14import {logEvent} from '#/lib/statsig/statsig'
15-import {isWeb} from '#/platform/detection'
16import {emitSoftReset} from '#/state/events'
17import {
18 type SavedFeedSourceInfo,
···37import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
38import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
39import * as Layout from '#/components/Layout'
040import {useDemoMode} from '#/storage/hooks/demo-mode'
4142type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
···48 usePinnedFeedsInfos()
4950 React.useEffect(() => {
51- if (isWeb && !currentAccount) {
52 const getParams = new URLSearchParams(window.location.search)
53 const splash = getParams.get('splash')
54 if (splash === 'true') {
···12 type NativeStackScreenProps,
13} from '#/lib/routes/types'
14import {logEvent} from '#/lib/statsig/statsig'
015import {emitSoftReset} from '#/state/events'
16import {
17 type SavedFeedSourceInfo,
···36import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
37import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
38import * as Layout from '#/components/Layout'
39+import {IS_WEB} from '#/env'
40import {useDemoMode} from '#/storage/hooks/demo-mode'
4142type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
···48 usePinnedFeedsInfos()
4950 React.useEffect(() => {
51+ if (IS_WEB && !currentAccount) {
52 const getParams = new URLSearchParams(window.location.search)
53 const splash = getParams.get('splash')
54 if (splash === 'true') {
+3-3
src/view/screens/Notifications.tsx
···14} from '#/lib/routes/types'
15import {s} from '#/lib/styles'
16import {logger} from '#/logger'
17-import {isNative} from '#/platform/detection'
18import {emitSoftReset, listenSoftReset} from '#/state/events'
19import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
20import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
···39import * as Layout from '#/components/Layout'
40import {InlineLinkText, Link} from '#/components/Link'
41import {Loader} from '#/components/Loader'
04243// We don't currently persist this across reloads since
44// you gotta visit All to clear the badge anyway.
···197 // event handlers
198 // =
199 const scrollToTop = useCallback(() => {
200- scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0})
201 setMinimalShellMode(false)
202 }, [scrollElRef, setMinimalShellMode])
203···227 // on focus, check for latest, but only invalidate if the user
228 // isnt scrolled down to avoid moving content underneath them
229 let currentIsScrolledDown
230- if (isNative) {
231 currentIsScrolledDown = isScrolledDown
232 } else {
233 // On the web, this isn't always updated in time so
···14} from '#/lib/routes/types'
15import {s} from '#/lib/styles'
16import {logger} from '#/logger'
017import {emitSoftReset, listenSoftReset} from '#/state/events'
18import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
19import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
···38import * as Layout from '#/components/Layout'
39import {InlineLinkText, Link} from '#/components/Link'
40import {Loader} from '#/components/Loader'
41+import {IS_NATIVE} from '#/env'
4243// We don't currently persist this across reloads since
44// you gotta visit All to clear the badge anyway.
···197 // event handlers
198 // =
199 const scrollToTop = useCallback(() => {
200+ scrollElRef.current?.scrollToOffset({animated: IS_NATIVE, offset: 0})
201 setMinimalShellMode(false)
202 }, [scrollElRef, setMinimalShellMode])
203···227 // on focus, check for latest, but only invalidate if the user
228 // isnt scrolled down to avoid moving content underneath them
229 let currentIsScrolledDown
230+ if (IS_NATIVE) {
231 currentIsScrolledDown = isScrolledDown
232 } else {
233 // On the web, this isn't always updated in time so
+2-2
src/view/shell/Drawer.tsx
···13import {type NavigationProp} from '#/lib/routes/types'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {colors} from '#/lib/styles'
16-import {isWeb} from '#/platform/detection'
17import {emitSoftReset} from '#/state/events'
18import {useKawaiiMode} from '#/state/preferences/kawaii'
19import {useUnreadNotifications} from '#/state/queries/notifications/unread'
···55import {Text} from '#/components/Typography'
56import {useSimpleVerificationState} from '#/components/verification'
57import {VerificationCheck} from '#/components/verification/VerificationCheck'
05859const iconWidth = 26
60···165 (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => {
166 const state = navigation.getState()
167 setDrawerOpen(false)
168- if (isWeb) {
169 // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
170 if (tab === 'MyProfile') {
171 navigation.navigate('Profile', {name: currentAccount!.handle})
···13import {type NavigationProp} from '#/lib/routes/types'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {colors} from '#/lib/styles'
016import {emitSoftReset} from '#/state/events'
17import {useKawaiiMode} from '#/state/preferences/kawaii'
18import {useUnreadNotifications} from '#/state/queries/notifications/unread'
···54import {Text} from '#/components/Typography'
55import {useSimpleVerificationState} from '#/components/verification'
56import {VerificationCheck} from '#/components/verification/VerificationCheck'
57+import {IS_WEB} from '#/env'
5859const iconWidth = 26
60···165 (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => {
166 const state = navigation.getState()
167 setDrawerOpen(false)
168+ if (IS_WEB) {
169 // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
170 if (tab === 'MyProfile') {
171 navigation.navigate('Profile', {name: currentAccount!.handle})