Bluesky app fork with some witchin' additions 💫

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

* Add platform vars to env

* Replace /platform/detection with /env

authored by

Eric Bailey and committed by
GitHub
0589bd7d bd510d84

+789 -789
+2 -2
CLAUDE.md
··· 439 440 Platform detection: 441 ```tsx 442 - import {isWeb, isNative, isIOS, isAndroid} from '#/platform/detection' 443 444 - if (isNative) { 445 // Native-specific logic 446 } 447 ```
··· 439 440 Platform detection: 441 ```tsx 442 + import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env' 443 444 + if (IS_NATIVE) { 445 // Native-specific logic 446 } 447 ```
+5 -5
modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
··· 11 import {useSafeAreaInsets} from 'react-native-safe-area-context' 12 import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' 13 14 - import {isIOS} from '#/platform/detection' 15 import { 16 type BottomSheetState, 17 type BottomSheetViewProps, ··· 30 31 const NativeModule = requireNativeModule('BottomSheet') 32 33 - 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 } 92 93 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 155 156 - const sheetHeight = isIOS ? screenHeight - insets.top : screenHeight 157 158 return ( 159 <NativeView
··· 11 import {useSafeAreaInsets} from 'react-native-safe-area-context' 12 import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' 13 14 + import {IS_IOS} from '#/env' 15 import { 16 type BottomSheetState, 17 type BottomSheetViewProps, ··· 30 31 const NativeModule = requireNativeModule('BottomSheet') 32 33 + 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 } 92 93 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 155 156 + const sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight 157 158 return ( 159 <NativeView
+3 -3
src/App.native.tsx
··· 23 import {ThemeProvider} from '#/lib/ThemeContext' 24 import I18nProvider from '#/locale/i18nProvider' 25 import {logger} from '#/logger' 26 - import {isAndroid, isIOS} from '#/platform/detection' 27 import {Provider as A11yProvider} from '#/state/a11y' 28 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 29 import {Provider as DialogStateProvider} from '#/state/dialogs' ··· 69 import {ToastOutlet} from '#/components/Toast' 70 import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' 71 import {prefetchAgeAssuranceConfig} from '#/ageAssurance' 72 import { 73 prefetchLiveEvents, 74 Provider as LiveEventsProvider, ··· 79 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 80 81 SplashScreen.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,
··· 23 import {ThemeProvider} from '#/lib/ThemeContext' 24 import I18nProvider from '#/locale/i18nProvider' 25 import {logger} from '#/logger' 26 import {Provider as A11yProvider} from '#/state/a11y' 27 import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes' 28 import {Provider as DialogStateProvider} from '#/state/dialogs' ··· 68 import {ToastOutlet} from '#/components/Toast' 69 import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance' 70 import {prefetchAgeAssuranceConfig} from '#/ageAssurance' 71 + import {IS_ANDROID, IS_IOS} from '#/env' 72 import { 73 prefetchLiveEvents, 74 Provider as LiveEventsProvider, ··· 79 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 80 81 SplashScreen.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
··· 44 import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' 45 import {bskyTitle} from '#/lib/strings/headings' 46 import {logger} from '#/logger' 47 - import {isNative, isWeb} from '#/platform/detection' 48 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 49 import {useSession} from '#/state/session' 50 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 138 EmailDialogScreenID, 139 useEmailDialogControl, 140 } from '#/components/dialogs/EmailDialog' 141 import {router} from '#/routes' 142 import {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 } 848 849 - if (isNative) { 850 if (name === 'Search') { 851 return buildStateObject('SearchTab', 'Search', params) 852 } ··· 921 ) 922 923 async function handlePushNotificationEntry() { 924 - if (!isNative) return 925 926 // 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 }) 1105 1106 - if (isWeb) { 1107 const referrerInfo = Referrer.getReferrerInfo() 1108 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { 1109 logEvent('deepLink:referrerReceived', {
··· 44 import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig' 45 import {bskyTitle} from '#/lib/strings/headings' 46 import {logger} from '#/logger' 47 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 48 import {useSession} from '#/state/session' 49 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 137 EmailDialogScreenID, 138 useEmailDialogControl, 139 } from '#/components/dialogs/EmailDialog' 140 + import {IS_NATIVE, IS_WEB} from '#/env' 141 import {router} from '#/routes' 142 import {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 } 848 849 + if (IS_NATIVE) { 850 if (name === 'Search') { 851 return buildStateObject('SearchTab', 'Search', params) 852 } ··· 921 ) 922 923 async function handlePushNotificationEntry() { 924 + if (!IS_NATIVE) return 925 926 // 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 }) 1105 1106 + 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' 11 import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 12 import {logger} from '#/logger' 13 - import {isWeb} from '#/platform/detection' 14 - import {isNative} from '#/platform/detection' 15 import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 16 import {useSessionApi} from '#/state/session' 17 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' ··· 38 isLegacyBirthdateBug, 39 useAgeAssuranceRegionConfig, 40 } from '#/ageAssurance/util' 41 import {useDeviceGeolocationApi} from '#/geolocation' 42 43 const textStyles = [a.text_md, a.leading_snug] ··· 74 }, []) 75 76 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 )} 360 361 <View style={[a.gap_xs]}> 362 - {isNative && ( 363 <> 364 <Admonition> 365 <Trans>
··· 10 } from '#/lib/hooks/useCreateSupportLink' 11 import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 12 import {logger} from '#/logger' 13 import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 14 import {useSessionApi} from '#/state/session' 15 import {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' 41 import {useDeviceGeolocationApi} from '#/geolocation' 42 43 const textStyles = [a.text_md, a.leading_snug] ··· 74 }, []) 75 76 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 )} 360 361 <View style={[a.gap_xs]}> 362 + {IS_NATIVE && ( 363 <> 364 <Admonition> 365 <Trans>
+4 -4
src/ageAssurance/components/RedirectOverlay.tsx
··· 15 import {retry} from '#/lib/async/retry' 16 import {wait} from '#/lib/async/wait' 17 import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 18 - import {isWeb} from '#/platform/detection' 19 - import {isIOS} from '#/platform/detection' 20 import {useAgent, useSession} from '#/state/session' 21 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 22 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' ··· 28 import {Text} from '#/components/Typography' 29 import {refetchAgeAssuranceServerState} from '#/ageAssurance' 30 import {logger} from '#/ageAssurance' 31 32 export type RedirectOverlayState = { 33 result: 'success' | 'unknown' ··· 92 actorDid: params.get('actorDid') ?? undefined, 93 }) 94 95 - 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,
··· 15 import {retry} from '#/lib/async/retry' 16 import {wait} from '#/lib/async/wait' 17 import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 18 import {useAgent, useSession} from '#/state/session' 19 import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' 20 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' ··· 26 import {Text} from '#/components/Typography' 27 import {refetchAgeAssuranceServerState} from '#/ageAssurance' 28 import {logger} from '#/ageAssurance' 29 + import {IS_WEB} from '#/env' 30 + import {IS_IOS} from '#/env' 31 32 export type RedirectOverlayState = { 33 result: 'success' | 'unknown' ··· 92 actorDid: params.get('actorDid') ?? undefined, 93 }) 94 95 + 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
··· 1 import {type TextStyle} from 'react-native' 2 3 - import {isAndroid, isWeb} from '#/platform/detection' 4 import {type Device, device} from '#/storage' 5 6 const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"` ··· 39 */ 40 export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') { 41 if (fontFamily === 'theme') { 42 - if (isAndroid) { 43 style.fontFamily = 44 { 45 400: 'Inter-Regular', ··· 71 } 72 } 73 74 - 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
··· 1 import {type TextStyle} from 'react-native' 2 3 + import {IS_ANDROID, IS_WEB} from '#/env' 4 import {type Device, device} from '#/storage' 5 6 const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"` ··· 39 */ 40 export 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 } 73 74 + 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
··· 4 import {UITextView} from 'react-native-uitextview' 5 import createEmojiRegex from 'emoji-regex' 6 7 - import {isNative} from '#/platform/detection' 8 - import {isIOS} from '#/platform/detection' 9 import {type Alf, applyFonts, atoms, flatten} from '#/alf' 10 11 /** 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 => {
··· 4 import {UITextView} from 'react-native-uitextview' 5 import createEmojiRegex from 'emoji-regex' 6 7 import {type Alf, applyFonts, atoms, flatten} from '#/alf' 8 + import {IS_NATIVE} from '#/env' 9 + import {IS_IOS} from '#/env' 10 11 /** 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
··· 2 import {type Theme} from '@bsky.app/alf' 3 4 import {logger} from '#/logger' 5 - import {isAndroid} from '#/platform/detection' 6 7 export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) { 8 - if (isAndroid) { 9 try { 10 if (themeType === 'theme') { 11 SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
··· 2 import {type Theme} from '@bsky.app/alf' 3 4 import {logger} from '#/logger' 5 + import {IS_ANDROID} from '#/env' 6 7 export 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
··· 2 import {type ColorSchemeName, useColorScheme} from 'react-native' 3 import {type ThemeName} from '@bsky.app/alf' 4 5 - import {isWeb} from '#/platform/detection' 6 import {useThemePrefs} from '#/state/shell' 7 import {dark, dim, light} from '#/alf/themes' 8 9 export function useColorModeTheme(): ThemeName { 10 const theme = useThemeName() ··· 40 41 function 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
··· 2 import {type ColorSchemeName, useColorScheme} from 'react-native' 3 import {type ThemeName} from '@bsky.app/alf' 4 5 import {useThemePrefs} from '#/state/shell' 6 import {dark, dim, light} from '#/alf/themes' 7 + import {IS_WEB} from '#/env' 8 9 export function useColorModeTheme(): ThemeName { 10 const theme = useThemeName() ··· 40 41 function 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
+5 -5
src/components/ContextMenu/index.tsx
··· 49 import {useHaptics} from '#/lib/haptics' 50 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 51 import {logger} from '#/logger' 52 - import {isAndroid, isIOS} from '#/platform/detection' 53 import {atoms as a, platform, tokens, useTheme} from '#/alf' 54 import { 55 Context, ··· 71 import {useInteractionState} from '#/components/hooks/useInteractionState' 72 import {createPortalGroup} from '#/components/Portal' 73 import {Text} from '#/components/Typography' 74 import {Backdrop} from './Backdrop' 75 76 export { ··· 81 const {Provider: PortalProvider, Outlet, Portal} = createPortalGroup() 82 83 const SPRING_IN: WithSpringConfig = { 84 - mass: isIOS ? 1.25 : 0.75, 85 damping: 50, 86 stiffness: 1100, 87 restDisplacementThreshold: 0.01, 88 } 89 90 const SPRING_OUT: WithSpringConfig = { 91 - mass: isIOS ? 1.25 : 0.75, 92 damping: 150, 93 stiffness: 1000, 94 restDisplacementThreshold: 0.01, ··· 209 ) 210 211 useEffect(() => { 212 - if (isAndroid && context.isOpen) { 213 const listener = BackHandler.addEventListener('hardwareBackPress', () => { 214 context.close() 215 return true ··· 331 <GestureDetector gesture={composedGestures}> 332 <View ref={ref} style={[{opacity: context.isOpen ? 0 : 1}, style]}> 333 {children({ 334 - isNative: true, 335 control: {isOpen: context.isOpen, open}, 336 state: { 337 pressed: false,
··· 49 import {useHaptics} from '#/lib/haptics' 50 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 51 import {logger} from '#/logger' 52 import {atoms as a, platform, tokens, useTheme} from '#/alf' 53 import { 54 Context, ··· 70 import {useInteractionState} from '#/components/hooks/useInteractionState' 71 import {createPortalGroup} from '#/components/Portal' 72 import {Text} from '#/components/Typography' 73 + import {IS_ANDROID, IS_IOS} from '#/env' 74 import {Backdrop} from './Backdrop' 75 76 export { ··· 81 const {Provider: PortalProvider, Outlet, Portal} = createPortalGroup() 82 83 const SPRING_IN: WithSpringConfig = { 84 + mass: IS_IOS ? 1.25 : 0.75, 85 damping: 50, 86 stiffness: 1100, 87 restDisplacementThreshold: 0.01, 88 } 89 90 const SPRING_OUT: WithSpringConfig = { 91 + mass: IS_IOS ? 1.25 : 0.75, 92 damping: 150, 93 stiffness: 1000, 94 restDisplacementThreshold: 0.01, ··· 209 ) 210 211 useEffect(() => { 212 + if (IS_ANDROID && context.isOpen) { 213 const listener = BackHandler.addEventListener('hardwareBackPress', () => { 214 context.close() 215 return true ··· 331 <GestureDetector gesture={composedGestures}> 332 <View ref={ref} style={[{opacity: context.isOpen ? 0 : 1}, style]}> 333 {children({ 334 + IS_NATIVE: true, 335 control: {isOpen: context.isOpen, open}, 336 state: { 337 pressed: false,
+2 -2
src/components/ContextMenu/types.ts
··· 85 } 86 export type TriggerChildProps = 87 | { 88 - isNative: true 89 control: { 90 isOpen: boolean 91 open: (mode: 'full' | 'auxiliary-only') => void ··· 115 } 116 } 117 | { 118 - isNative: false 119 control: Dialog.DialogOuterProps['control'] 120 state: { 121 hovered: false
··· 85 } 86 export type TriggerChildProps = 87 | { 88 + IS_NATIVE: true 89 control: { 90 isOpen: boolean 91 open: (mode: 'full' | 'auxiliary-only') => void ··· 115 } 116 } 117 | { 118 + IS_NATIVE: false 119 control: Dialog.DialogOuterProps['control'] 120 state: { 121 hovered: false
+1 -1
src/components/Dialog/context.ts
··· 18 19 export const Context = createContext<DialogContextProps>({ 20 close: () => {}, 21 - isNativeDialog: false, 22 nativeSnapPoint: BottomSheetSnapPoint.Hidden, 23 disableDrag: false, 24 setDisableDrag: () => {},
··· 18 19 export const Context = createContext<DialogContextProps>({ 20 close: () => {}, 21 + IS_NATIVEDialog: false, 22 nativeSnapPoint: BottomSheetSnapPoint.Hidden, 23 disableDrag: false, 24 setDisableDrag: () => {},
+11 -11
src/components/Dialog/index.tsx
··· 26 import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 27 import {ScrollProvider} from '#/lib/ScrollContext' 28 import {logger} from '#/logger' 29 - import {isAndroid, isIOS} from '#/platform/detection' 30 import {useA11y} from '#/state/a11y' 31 import {useDialogStateControlContext} from '#/state/dialogs' 32 import {List, type ListMethods, type ListProps} from '#/view/com/util/List' ··· 39 type DialogOuterProps, 40 } from '#/components/Dialog/types' 41 import {createInput} from '#/components/forms/TextField' 42 import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' 43 import { 44 type BottomSheetSnapPointChangeEvent, ··· 154 const context = React.useMemo( 155 () => ({ 156 close, 157 - isNativeDialog: true, 158 nativeSnapPoint: snapPoint, 159 disableDrag, 160 setDisableDrag, ··· 209 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 210 const insets = useSafeAreaInsets() 211 212 - useEnableKeyboardController(isIOS) 213 214 const [keyboardHeight, setKeyboardHeight] = React.useState(0) 215 ··· 224 ) 225 226 let paddingBottom = 0 227 - if (isIOS) { 228 paddingBottom += keyboardHeight / 4 229 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 230 paddingBottom += insets.bottom + tokens.space.md ··· 240 } 241 242 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { 243 - if (!isAndroid) { 244 return 245 } 246 const {contentOffset} = e.nativeEvent ··· 260 contentContainerStyle, 261 ]} 262 ref={ref} 263 - showsVerticalScrollIndicator={isAndroid ? false : undefined} 264 {...props} 265 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 266 bottomOffset={30} 267 scrollEventThrottle={50} 268 - onScroll={isAndroid ? onScroll : undefined} 269 keyboardShouldPersistTaps="handled" 270 // TODO: figure out why this positions the header absolutely (rather than stickily) 271 // on Android. fine to disable for now, because we don't have any ··· 289 const insets = useSafeAreaInsets() 290 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 291 292 - useEnableKeyboardController(isIOS) 293 294 const onScroll = (e: ScrollEvent) => { 295 'worklet' 296 - if (!isAndroid) { 297 return 298 } 299 const {contentOffset} = e ··· 311 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 312 ListFooterComponent={<View style={{height: insets.bottom + 100}} />} 313 ref={ref} 314 - showsVerticalScrollIndicator={isAndroid ? false : undefined} 315 {...props} 316 style={[a.h_full, style]} 317 /> ··· 326 const {height} = useReanimatedKeyboardAnimation() 327 328 const animatedStyle = useAnimatedStyle(() => { 329 - if (!isIOS) return {} 330 return { 331 transform: [{translateY: Math.min(0, height.get() + bottom - 10)}], 332 }
··· 26 import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 27 import {ScrollProvider} from '#/lib/ScrollContext' 28 import {logger} from '#/logger' 29 import {useA11y} from '#/state/a11y' 30 import {useDialogStateControlContext} from '#/state/dialogs' 31 import {List, type ListMethods, type ListProps} from '#/view/com/util/List' ··· 38 type DialogOuterProps, 39 } from '#/components/Dialog/types' 40 import {createInput} from '#/components/forms/TextField' 41 + import {IS_ANDROID, IS_IOS} from '#/env' 42 import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' 43 import { 44 type BottomSheetSnapPointChangeEvent, ··· 154 const context = React.useMemo( 155 () => ({ 156 close, 157 + IS_NATIVEDialog: true, 158 nativeSnapPoint: snapPoint, 159 disableDrag, 160 setDisableDrag, ··· 209 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 210 const insets = useSafeAreaInsets() 211 212 + useEnableKeyboardController(IS_IOS) 213 214 const [keyboardHeight, setKeyboardHeight] = React.useState(0) 215 ··· 224 ) 225 226 let paddingBottom = 0 227 + if (IS_IOS) { 228 paddingBottom += keyboardHeight / 4 229 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 230 paddingBottom += insets.bottom + tokens.space.md ··· 240 } 241 242 const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { 243 + if (!IS_ANDROID) { 244 return 245 } 246 const {contentOffset} = e.nativeEvent ··· 260 contentContainerStyle, 261 ]} 262 ref={ref} 263 + showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} 264 {...props} 265 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 266 bottomOffset={30} 267 scrollEventThrottle={50} 268 + onScroll={IS_ANDROID ? onScroll : undefined} 269 keyboardShouldPersistTaps="handled" 270 // TODO: figure out why this positions the header absolutely (rather than stickily) 271 // on Android. fine to disable for now, because we don't have any ··· 289 const insets = useSafeAreaInsets() 290 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 291 292 + useEnableKeyboardController(IS_IOS) 293 294 const onScroll = (e: ScrollEvent) => { 295 'worklet' 296 + if (!IS_ANDROID) { 297 return 298 } 299 const {contentOffset} = e ··· 311 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} 312 ListFooterComponent={<View style={{height: insets.bottom + 100}} />} 313 ref={ref} 314 + showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} 315 {...props} 316 style={[a.h_full, style]} 317 /> ··· 326 const {height} = useReanimatedKeyboardAnimation() 327 328 const animatedStyle = useAnimatedStyle(() => { 329 + if (!IS_IOS) return {} 330 return { 331 transform: [{translateY: Math.min(0, height.get() + bottom - 10)}], 332 }
+1 -1
src/components/Dialog/index.web.tsx
··· 98 const context = React.useMemo( 99 () => ({ 100 close, 101 - isNativeDialog: false, 102 nativeSnapPoint: 0, 103 disableDrag: false, 104 setDisableDrag: () => {},
··· 98 const context = React.useMemo( 99 () => ({ 100 close, 101 + IS_NATIVEDialog: false, 102 nativeSnapPoint: 0, 103 disableDrag: false, 104 setDisableDrag: () => {},
+2 -2
src/components/Dialog/sheet-wrapper.ts
··· 1 import {useCallback} from 'react' 2 import {SystemBars} from 'react-native-edge-to-edge' 3 4 - import {isIOS} from '#/platform/detection' 5 6 /** 7 * If we're calling a system API like the image picker that opens a sheet ··· 9 */ 10 export 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',
··· 1 import {useCallback} from 'react' 2 import {SystemBars} from 'react-native-edge-to-edge' 3 4 + import {IS_IOS} from '#/env' 5 6 /** 7 * If we're calling a system API like the image picker that opens a sheet ··· 9 */ 10 export 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',
+1 -1
src/components/Dialog/types.ts
··· 39 40 export type DialogContextProps = { 41 close: DialogControlProps['close'] 42 - isNativeDialog: boolean 43 nativeSnapPoint: BottomSheetSnapPoint 44 disableDrag: boolean 45 setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
··· 39 40 export type DialogContextProps = { 41 close: DialogControlProps['close'] 42 + IS_NATIVEDialog: boolean 43 nativeSnapPoint: BottomSheetSnapPoint 44 disableDrag: boolean 45 setDisableDrag: React.Dispatch<React.SetStateAction<boolean>>
+3 -3
src/components/FeedInterstitials.tsx
··· 10 import {logEvent, useGate} from '#/lib/statsig/statsig' 11 import {logger} from '#/logger' 12 import {type MetricEvents} from '#/logger/metrics' 13 - import {isIOS} from '#/platform/detection' 14 import {useModerationOpts} from '#/state/preferences/moderation-opts' 15 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 16 import {type FeedDescriptor} from '#/state/queries/post-feed' ··· 39 import {InlineLinkText} from '#/components/Link' 40 import * as ProfileCard from '#/components/ProfileCard' 41 import {Text} from '#/components/Typography' 42 import type * as bsky from '#/types/bsky' 43 import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog' 44 import {ProgressGuideList} from './ProgressGuide/List' ··· 692 t.atoms.border_contrast_low, 693 t.atoms.bg_contrast_25, 694 ]} 695 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 696 <View 697 style={[ 698 a.px_lg, ··· 701 a.align_center, 702 a.justify_between, 703 ]} 704 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 705 <Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}> 706 {isFeedContext ? ( 707 <Trans>Suggested for you</Trans>
··· 10 import {logEvent, useGate} from '#/lib/statsig/statsig' 11 import {logger} from '#/logger' 12 import {type MetricEvents} from '#/logger/metrics' 13 import {useModerationOpts} from '#/state/preferences/moderation-opts' 14 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 15 import {type FeedDescriptor} from '#/state/queries/post-feed' ··· 38 import {InlineLinkText} from '#/components/Link' 39 import * as ProfileCard from '#/components/ProfileCard' 40 import {Text} from '#/components/Typography' 41 + import {IS_IOS} from '#/env' 42 import type * as bsky from '#/types/bsky' 43 import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog' 44 import {ProgressGuideList} from './ProgressGuide/List' ··· 692 t.atoms.border_contrast_low, 693 t.atoms.bg_contrast_25, 694 ]} 695 + pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 696 <View 697 style={[ 698 a.px_lg, ··· 701 a.align_center, 702 a.justify_between, 703 ]} 704 + pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 705 <Text style={[a.text_sm, a.font_semi_bold, t.atoms.text]}> 706 {isFeedContext ? ( 707 <Trans>Suggested for you</Trans>
+3 -3
src/components/InterestTabs.tsx
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 12 - import {isWeb} from '#/platform/detection' 13 import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView' 14 import {atoms as a, tokens, useTheme, web} from '#/alf' 15 import {transparentifyColor} from '#/alf/util/colorGeneration' ··· 19 ArrowRight_Stroke2_Corner0_Rounded as ArrowRight, 20 } from '#/components/icons/Arrow' 21 import {Text} from '#/components/Typography' 22 23 /** 24 * Tab component that automatically scrolls the selected tab into view - used for interests ··· 236 ) 237 })} 238 </DraggableScrollView> 239 - {isWeb && canScrollLeft && ( 240 <View 241 style={[ 242 a.absolute, ··· 270 </Button> 271 </View> 272 )} 273 - {isWeb && canScrollRight && ( 274 <View 275 style={[ 276 a.absolute,
··· 9 import {useLingui} from '@lingui/react' 10 11 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 12 import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView' 13 import {atoms as a, tokens, useTheme, web} from '#/alf' 14 import {transparentifyColor} from '#/alf/util/colorGeneration' ··· 18 ArrowRight_Stroke2_Corner0_Rounded as ArrowRight, 19 } from '#/components/icons/Arrow' 20 import {Text} from '#/components/Typography' 21 + import {IS_WEB} from '#/env' 22 23 /** 24 * Tab component that automatically scrolls the selected tab into view - used for interests ··· 236 ) 237 })} 238 </DraggableScrollView> 239 + {IS_WEB && canScrollLeft && ( 240 <View 241 style={[ 242 a.absolute, ··· 270 </Button> 271 </View> 272 )} 273 + {IS_WEB && canScrollRight && ( 274 <View 275 style={[ 276 a.absolute,
+3 -3
src/components/InternationalPhoneCodeSelect.tsx
··· 10 INTERNATIONAL_TELEPHONE_CODES, 11 } from '#/lib/international-telephone-codes' 12 import {regionName} from '#/locale/helpers' 13 - import {isWeb} from '#/platform/detection' 14 import {atoms as a, web} from '#/alf' 15 import * as Select from '#/components/Select' 16 import {useGeolocation} from '#/geolocation' 17 18 /** ··· 84 <Select.Item value={item.value} label={item.label}> 85 <Select.ItemIndicator /> 86 <Select.ItemText style={[a.flex_1]} emoji> 87 - {isWeb ? <Flag {...item} /> : item.unicodeFlag + ' '} 88 {item.name} 89 </Select.ItemText> 90 <Select.ItemText style={[a.text_right]}> ··· 101 } 102 103 function Flag({unicodeFlag, svgFlag}: {unicodeFlag: string; svgFlag: any}) { 104 - if (isWeb) { 105 return ( 106 <Image 107 source={svgFlag}
··· 10 INTERNATIONAL_TELEPHONE_CODES, 11 } from '#/lib/international-telephone-codes' 12 import {regionName} from '#/locale/helpers' 13 import {atoms as a, web} from '#/alf' 14 import * as Select from '#/components/Select' 15 + import {IS_WEB} from '#/env' 16 import {useGeolocation} from '#/geolocation' 17 18 /** ··· 84 <Select.Item value={item.value} label={item.label}> 85 <Select.ItemIndicator /> 86 <Select.ItemText style={[a.flex_1]} emoji> 87 + {IS_WEB ? <Flag {...item} /> : item.unicodeFlag + ' '} 88 {item.name} 89 </Select.ItemText> 90 <Select.ItemText style={[a.text_right]}> ··· 101 } 102 103 function Flag({unicodeFlag, svgFlag}: {unicodeFlag: string; svgFlag: any}) { 104 + if (IS_WEB) { 105 return ( 106 <Image 107 source={svgFlag}
+4 -4
src/components/Layout/Header/index.tsx
··· 6 7 import {HITSLOP_30} from '#/lib/constants' 8 import {type NavigationProp} from '#/lib/routes/types' 9 - import {isIOS} from '#/platform/detection' 10 import {useSetDrawerOpen} from '#/state/shell' 11 import { 12 atoms as a, ··· 29 } from '#/components/Layout/const' 30 import {ScrollbarOffsetContext} from '#/components/Layout/context' 31 import {Text} from '#/components/Typography' 32 33 export function Outer({ 34 children, ··· 91 style={[ 92 a.flex_1, 93 a.justify_center, 94 - isIOS && align === 'platform' && a.align_center, 95 {minHeight: HEADER_SLOT_SIZE}, 96 ]}> 97 <AlignmentContext.Provider value={align}> ··· 186 a.text_lg, 187 a.font_semi_bold, 188 a.leading_tight, 189 - isIOS && align === 'platform' && a.text_center, 190 gtMobile && a.text_xl, 191 style, 192 ]} ··· 205 style={[ 206 a.text_sm, 207 a.leading_snug, 208 - isIOS && align === 'platform' && a.text_center, 209 t.atoms.text_contrast_medium, 210 ]} 211 numberOfLines={2}>
··· 6 7 import {HITSLOP_30} from '#/lib/constants' 8 import {type NavigationProp} from '#/lib/routes/types' 9 import {useSetDrawerOpen} from '#/state/shell' 10 import { 11 atoms as a, ··· 28 } from '#/components/Layout/const' 29 import {ScrollbarOffsetContext} from '#/components/Layout/context' 30 import {Text} from '#/components/Typography' 31 + import {IS_IOS} from '#/env' 32 33 export function Outer({ 34 children, ··· 91 style={[ 92 a.flex_1, 93 a.justify_center, 94 + IS_IOS && align === 'platform' && a.align_center, 95 {minHeight: HEADER_SLOT_SIZE}, 96 ]}> 97 <AlignmentContext.Provider value={align}> ··· 186 a.text_lg, 187 a.font_semi_bold, 188 a.leading_tight, 189 + IS_IOS && align === 'platform' && a.text_center, 190 gtMobile && a.text_xl, 191 style, 192 ]} ··· 205 style={[ 206 a.text_sm, 207 a.leading_snug, 208 + IS_IOS && align === 'platform' && a.text_center, 209 t.atoms.text_contrast_medium, 210 ]} 211 numberOfLines={2}>
+4 -4
src/components/Layout/index.tsx
··· 11 } from 'react-native-reanimated' 12 import {useSafeAreaInsets} from 'react-native-safe-area-context' 13 14 - import {isWeb} from '#/platform/detection' 15 import {useShellLayout} from '#/state/shell/shell-layout' 16 import { 17 atoms as a, ··· 23 import {useDialogContext} from '#/components/Dialog' 24 import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const' 25 import {ScrollbarOffsetContext} from '#/components/Layout/context' 26 27 export * from '#/components/Layout/const' 28 export * as Header from '#/components/Layout/Header' ··· 43 const {top} = useSafeAreaInsets() 44 return ( 45 <> 46 - {isWeb && <WebCenterBorders />} 47 <View 48 style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]} 49 {...props} ··· 98 contentContainerStyle, 99 ]} 100 {...props}> 101 - {isWeb ? ( 102 <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 103 {/* @ts-expect-error web only -esb */} 104 {children} ··· 145 ]} 146 keyboardShouldPersistTaps="handled" 147 {...props}> 148 - {isWeb ? <Center>{children}</Center> : children} 149 </KeyboardAwareScrollView> 150 ) 151 })
··· 11 } from 'react-native-reanimated' 12 import {useSafeAreaInsets} from 'react-native-safe-area-context' 13 14 import {useShellLayout} from '#/state/shell/shell-layout' 15 import { 16 atoms as a, ··· 22 import {useDialogContext} from '#/components/Dialog' 23 import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const' 24 import {ScrollbarOffsetContext} from '#/components/Layout/context' 25 + import {IS_WEB} from '#/env' 26 27 export * from '#/components/Layout/const' 28 export * as Header from '#/components/Layout/Header' ··· 43 const {top} = useSafeAreaInsets() 44 return ( 45 <> 46 + {IS_WEB && <WebCenterBorders />} 47 <View 48 style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]} 49 {...props} ··· 98 contentContainerStyle, 99 ]} 100 {...props}> 101 + {IS_WEB ? ( 102 <Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}> 103 {/* @ts-expect-error web only -esb */} 104 {children} ··· 145 ]} 146 keyboardShouldPersistTaps="handled" 147 {...props}> 148 + {IS_WEB ? <Center>{children}</Center> : children} 149 </KeyboardAwareScrollView> 150 ) 151 })
+11 -11
src/components/Link.tsx
··· 18 isExternalUrl, 19 linkRequiresWarning, 20 } from '#/lib/strings/url-helpers' 21 - import {isNative, isWeb} from '#/platform/detection' 22 import {useModalControls} from '#/state/modals' 23 import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf' 24 import {Button, type ButtonProps} from '#/components/Button' 25 import {useInteractionState} from '#/components/hooks/useInteractionState' 26 import {Text, type TextProps} from '#/components/Typography' 27 import {router} from '#/routes' 28 import {useGlobalDialogsControlContext} from './dialogs/Context' 29 ··· 130 linkRequiresWarning(href, displayText), 131 ) 132 133 - if (isWeb) { 134 e.preventDefault() 135 } 136 ··· 162 ] 163 164 // does not apply to web's flat navigator 165 - if (isNative && screen !== 'NotFound') { 166 const state = navigation.getState() 167 // if screen is not in the current navigator, it means it's 168 // most likely a tab screen. note: state can be undefined ··· 246 (e: GestureResponderEvent) => { 247 const exitEarlyIfFalse = outerOnLongPress?.(e) 248 if (exitEarlyIfFalse === false) return 249 - return isNative && shareOnLongPress ? handleLongPress() : undefined 250 }, 251 [outerOnLongPress, handleLongPress, shareOnLongPress], 252 ) ··· 501 onPress, 502 ...props 503 }: Omit<InlineLinkProps, 'onLongPress'>) { 504 - return isWeb ? ( 505 <InlineLinkText {...props} to={to} onPress={onPress}> 506 {children} 507 </InlineLinkText> ··· 547 ): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} { 548 return { 549 onPress(e: GestureResponderEvent) { 550 - if (!isWeb || !isModifiedClickEvent(e)) { 551 e.preventDefault() 552 onPressHandler(e) 553 return false ··· 561 * intends to deviate from default behavior. 562 */ 563 export function isClickEventWithMetaKey(e: GestureResponderEvent) { 564 - if (!isWeb) return false 565 const event = e as unknown as MouseEvent 566 return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey 567 } ··· 570 * Determines if the web click target is anything other than `_self` 571 */ 572 export function isClickTargetExternal(e: GestureResponderEvent) { 573 - if (!isWeb) return false 574 const event = e as unknown as MouseEvent 575 const el = event.currentTarget as HTMLAnchorElement 576 return el && el.target && el.target !== '_self' ··· 582 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} 583 */ 584 export function isModifiedClickEvent(e: GestureResponderEvent): boolean { 585 - if (!isWeb) return false 586 const event = e as unknown as MouseEvent 587 const isPrimaryButton = event.button === 0 588 return ( ··· 596 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} 597 */ 598 export function shouldClickOpenNewTab(e: GestureResponderEvent) { 599 - if (!isWeb) return false 600 const event = e as unknown as MouseEvent 601 - const isMiddleClick = isWeb && event.button === 1 602 return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick 603 }
··· 18 isExternalUrl, 19 linkRequiresWarning, 20 } from '#/lib/strings/url-helpers' 21 import {useModalControls} from '#/state/modals' 22 import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf' 23 import {Button, type ButtonProps} from '#/components/Button' 24 import {useInteractionState} from '#/components/hooks/useInteractionState' 25 import {Text, type TextProps} from '#/components/Typography' 26 + import {IS_NATIVE, IS_WEB} from '#/env' 27 import {router} from '#/routes' 28 import {useGlobalDialogsControlContext} from './dialogs/Context' 29 ··· 130 linkRequiresWarning(href, displayText), 131 ) 132 133 + if (IS_WEB) { 134 e.preventDefault() 135 } 136 ··· 162 ] 163 164 // does not apply to web's flat navigator 165 + if (IS_NATIVE && screen !== 'NotFound') { 166 const state = navigation.getState() 167 // if screen is not in the current navigator, it means it's 168 // most likely a tab screen. note: state can be undefined ··· 246 (e: GestureResponderEvent) => { 247 const exitEarlyIfFalse = outerOnLongPress?.(e) 248 if (exitEarlyIfFalse === false) return 249 + return IS_NATIVE && shareOnLongPress ? handleLongPress() : undefined 250 }, 251 [outerOnLongPress, handleLongPress, shareOnLongPress], 252 ) ··· 501 onPress, 502 ...props 503 }: Omit<InlineLinkProps, 'onLongPress'>) { 504 + return IS_WEB ? ( 505 <InlineLinkText {...props} to={to} onPress={onPress}> 506 {children} 507 </InlineLinkText> ··· 547 ): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} { 548 return { 549 onPress(e: GestureResponderEvent) { 550 + if (!IS_WEB || !isModifiedClickEvent(e)) { 551 e.preventDefault() 552 onPressHandler(e) 553 return false ··· 561 * intends to deviate from default behavior. 562 */ 563 export function isClickEventWithMetaKey(e: GestureResponderEvent) { 564 + if (!IS_WEB) return false 565 const event = e as unknown as MouseEvent 566 return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey 567 } ··· 570 * Determines if the web click target is anything other than `_self` 571 */ 572 export function isClickTargetExternal(e: GestureResponderEvent) { 573 + if (!IS_WEB) return false 574 const event = e as unknown as MouseEvent 575 const el = event.currentTarget as HTMLAnchorElement 576 return el && el.target && el.target !== '_self' ··· 582 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} 583 */ 584 export function isModifiedClickEvent(e: GestureResponderEvent): boolean { 585 + if (!IS_WEB) return false 586 const event = e as unknown as MouseEvent 587 const isPrimaryButton = event.button === 0 588 return ( ··· 596 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} 597 */ 598 export function shouldClickOpenNewTab(e: GestureResponderEvent) { 599 + if (!IS_WEB) return false 600 const event = e as unknown as MouseEvent 601 + const isMiddleClick = IS_WEB && event.button === 1 602 return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick 603 }
+5 -5
src/components/Menu/index.tsx
··· 10 import {useLingui} from '@lingui/react' 11 import flattenReactChildren from 'react-keyed-flatten-children' 12 13 - import {isAndroid, isIOS, isNative} from '#/platform/detection' 14 import {atoms as a, useTheme} from '#/alf' 15 import {Button, ButtonText} from '#/components/Button' 16 import * as Dialog from '#/components/Dialog' ··· 30 type TriggerProps, 31 } from '#/components/Menu/types' 32 import {Text} from '#/components/Typography' 33 34 export { 35 type DialogControlProps as MenuControlProps, ··· 70 } = useInteractionState() 71 72 return children({ 73 - isNative: true, 74 control: context.control, 75 state: { 76 hovered: false, ··· 111 <Dialog.ScrollableInner label={_(msg`Menu`)}> 112 <View style={[a.gap_lg]}> 113 {children} 114 - {isNative && showCancel && <Cancel />} 115 </View> 116 </Dialog.ScrollableInner> 117 </Context.Provider> ··· 137 onFocus={onFocus} 138 onBlur={onBlur} 139 onPress={async e => { 140 - if (isAndroid) { 141 /** 142 * Below fix for iOS doesn't work for Android, this does. 143 */ 144 onPress?.(e) 145 context.control.close() 146 - } else if (isIOS) { 147 /** 148 * Fixes a subtle bug on iOS 149 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127}
··· 10 import {useLingui} from '@lingui/react' 11 import flattenReactChildren from 'react-keyed-flatten-children' 12 13 import {atoms as a, useTheme} from '#/alf' 14 import {Button, ButtonText} from '#/components/Button' 15 import * as Dialog from '#/components/Dialog' ··· 29 type TriggerProps, 30 } from '#/components/Menu/types' 31 import {Text} from '#/components/Typography' 32 + import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env' 33 34 export { 35 type DialogControlProps as MenuControlProps, ··· 70 } = useInteractionState() 71 72 return children({ 73 + IS_NATIVE: true, 74 control: context.control, 75 state: { 76 hovered: false, ··· 111 <Dialog.ScrollableInner label={_(msg`Menu`)}> 112 <View style={[a.gap_lg]}> 113 {children} 114 + {IS_NATIVE && showCancel && <Cancel />} 115 </View> 116 </Dialog.ScrollableInner> 117 </Context.Provider> ··· 137 onFocus={onFocus} 138 onBlur={onBlur} 139 onPress={async e => { 140 + if (IS_ANDROID) { 141 /** 142 * Below fix for iOS doesn't work for Android, this does. 143 */ 144 onPress?.(e) 145 context.control.close() 146 + } else if (IS_IOS) { 147 /** 148 * Fixes a subtle bug on iOS 149 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127}
+1 -1
src/components/Menu/index.web.tsx
··· 138 <RadixTriggerPassThrough> 139 {props => 140 children({ 141 - isNative: false, 142 control, 143 state: { 144 hovered,
··· 138 <RadixTriggerPassThrough> 139 {props => 140 children({ 141 + IS_NATIVE: false, 142 control, 143 state: { 144 hovered,
+2 -2
src/components/Menu/types.ts
··· 43 } 44 export type TriggerChildProps = 45 | { 46 - isNative: true 47 control: Dialog.DialogOuterProps['control'] 48 state: { 49 /** ··· 73 } 74 } 75 | { 76 - isNative: false 77 control: Dialog.DialogOuterProps['control'] 78 state: { 79 hovered: boolean
··· 43 } 44 export type TriggerChildProps = 45 | { 46 + IS_NATIVE: true 47 control: Dialog.DialogOuterProps['control'] 48 state: { 49 /** ··· 73 } 74 } 75 | { 76 + IS_NATIVE: false 77 control: Dialog.DialogOuterProps['control'] 78 state: { 79 hovered: boolean
+2 -2
src/components/NewskieDialog.tsx
··· 8 import {HITSLOP_10} from '#/lib/constants' 9 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 10 import {sanitizeDisplayName} from '#/lib/strings/display-names' 11 - import {isNative} from '#/platform/detection' 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 import {useSession} from '#/state/session' 14 import {atoms as a, useTheme, web} from '#/alf' ··· 18 import {Newskie} from '#/components/icons/Newskie' 19 import * as StarterPackCard from '#/components/StarterPack/StarterPackCard' 20 import {Text} from '#/components/Typography' 21 22 export function NewskieDialog({ 23 profile, ··· 162 </StarterPackCard.Link> 163 ) : null} 164 165 - {isNative && ( 166 <Button 167 label={_(msg`Close`)} 168 color="secondary"
··· 8 import {HITSLOP_10} from '#/lib/constants' 9 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 10 import {sanitizeDisplayName} from '#/lib/strings/display-names' 11 import {useModerationOpts} from '#/state/preferences/moderation-opts' 12 import {useSession} from '#/state/session' 13 import {atoms as a, useTheme, web} from '#/alf' ··· 17 import {Newskie} from '#/components/icons/Newskie' 18 import * as StarterPackCard from '#/components/StarterPack/StarterPackCard' 19 import {Text} from '#/components/Typography' 20 + import {IS_NATIVE} from '#/env' 21 22 export function NewskieDialog({ 23 profile, ··· 162 </StarterPackCard.Link> 163 ) : null} 164 165 + {IS_NATIVE && ( 166 <Button 167 label={_(msg`Close`)} 168 color="secondary"
+3 -3
src/components/PolicyUpdateOverlay/Overlay.tsx
··· 7 import {LinearGradient} from 'expo-linear-gradient' 8 import {utils} from '@bsky.app/alf' 9 10 - import {isAndroid, isNative} from '#/platform/detection' 11 import {useA11y} from '#/state/a11y' 12 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 13 import {FocusScope} from '#/components/FocusScope' 14 import {LockScroll} from '#/components/LockScroll' 15 16 const 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 }, ··· 109 110 <FocusScope> 111 <View 112 - accessible={isAndroid} 113 role="dialog" 114 aria-role="dialog" 115 aria-label={label}
··· 7 import {LinearGradient} from 'expo-linear-gradient' 8 import {utils} from '@bsky.app/alf' 9 10 import {useA11y} from '#/state/a11y' 11 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 12 import {FocusScope} from '#/components/FocusScope' 13 import {LockScroll} from '#/components/LockScroll' 14 + import {IS_ANDROID, IS_NATIVE} from '#/env' 15 16 const 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 }, ··· 109 110 <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
··· 1 import {useEffect} from 'react' 2 import {View} from 'react-native' 3 4 - import {isIOS} from '#/platform/detection' 5 import {atoms as a} from '#/alf' 6 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 7 import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context' 8 import {Portal} from '#/components/PolicyUpdateOverlay/Portal' 9 import {Content} from '#/components/PolicyUpdateOverlay/updates/202508' 10 11 export {Provider} from '#/components/PolicyUpdateOverlay/context' 12 export {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>
··· 1 import {useEffect} from 'react' 2 import {View} from 'react-native' 3 4 import {atoms as a} from '#/alf' 5 import {FullWindowOverlay} from '#/components/FullWindowOverlay' 6 import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context' 7 import {Portal} from '#/components/PolicyUpdateOverlay/Portal' 8 import {Content} from '#/components/PolicyUpdateOverlay/updates/202508' 9 + import {IS_IOS} from '#/env' 10 11 export {Provider} from '#/components/PolicyUpdateOverlay/context' 12 export {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>
+2 -2
src/components/PolicyUpdateOverlay/updates/202508/index.tsx
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 - import {isAndroid} from '#/platform/detection' 7 import {useA11y} from '#/state/a11y' 8 import {atoms as a, useTheme} from '#/alf' 9 import {Button, ButtonText} from '#/components/Button' ··· 12 import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay' 13 import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState' 14 import {Text} from '#/components/Typography' 15 16 export function Content({state}: {state: PolicyUpdateState}) { 17 const t = useTheme() ··· 56 size: 'small', 57 } as const 58 59 - 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 )
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {useA11y} from '#/state/a11y' 7 import {atoms as a, useTheme} from '#/alf' 8 import {Button, ButtonText} from '#/components/Button' ··· 11 import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay' 12 import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState' 13 import {Text} from '#/components/Typography' 14 + import {IS_ANDROID} from '#/env' 15 16 export function Content({state}: {state: PolicyUpdateState}) { 17 const t = useTheme() ··· 56 size: 'small', 57 } as const 58 59 + 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 )
+5 -5
src/components/Post/Embed/ExternalEmbed/ExternalGif.tsx
··· 10 import {useLingui} from '@lingui/react' 11 12 import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 13 - import {isIOS, isNative, isWeb} from '#/platform/detection' 14 import {useExternalEmbedsPrefs} from '#/state/preferences' 15 import {atoms as a, useTheme} from '#/alf' 16 import {useDialogControl} from '#/components/Dialog' 17 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 18 import {Fill} from '#/components/Fill' 19 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 20 21 export function ExternalGif({ 22 link, ··· 66 // Control animation on native 67 setIsAnimating(prev => { 68 if (prev) { 69 - if (isNative) { 70 imageRef.current?.stopAnimating() 71 } 72 return false 73 } else { 74 - if (isNative) { 75 imageRef.current?.startAnimating() 76 } 77 return true ··· 112 <Image 113 source={{ 114 uri: 115 - !isPrefetched || (isWeb && !isAnimating) 116 ? link.thumb 117 : params.playerUri, 118 }} // Web uses the thumb to control playback ··· 123 accessibilityIgnoresInvertColors 124 accessibilityLabel={link.title} 125 accessibilityHint={link.title} 126 - cachePolicy={isIOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios 127 /> 128 129 {(!isPrefetched || !isAnimating) && (
··· 10 import {useLingui} from '@lingui/react' 11 12 import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 13 import {useExternalEmbedsPrefs} from '#/state/preferences' 14 import {atoms as a, useTheme} from '#/alf' 15 import {useDialogControl} from '#/components/Dialog' 16 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 17 import {Fill} from '#/components/Fill' 18 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 19 + import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 20 21 export function ExternalGif({ 22 link, ··· 66 // Control animation on native 67 setIsAnimating(prev => { 68 if (prev) { 69 + if (IS_NATIVE) { 70 imageRef.current?.stopAnimating() 71 } 72 return false 73 } else { 74 + if (IS_NATIVE) { 75 imageRef.current?.startAnimating() 76 } 77 return true ··· 112 <Image 113 source={{ 114 uri: 115 + !isPrefetched || (IS_WEB && !isAnimating) 116 ? link.thumb 117 : params.playerUri, 118 }} // Web uses the thumb to control playback ··· 123 accessibilityIgnoresInvertColors 124 accessibilityLabel={link.title} 125 accessibilityHint={link.title} 126 + cachePolicy={IS_IOS ? 'disk' : 'memory-disk'} // cant control playback with memory-disk on ios 127 /> 128 129 {(!isPrefetched || !isAnimating) && (
+2 -2
src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx
··· 26 type EmbedPlayerParams, 27 getPlayerAspect, 28 } from '#/lib/strings/embed-player' 29 - import {isNative} from '#/platform/detection' 30 import {useExternalEmbedsPrefs} from '#/state/preferences' 31 import {EventStopper} from '#/view/com/util/EventStopper' 32 import {atoms as a, useTheme} from '#/alf' ··· 34 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 35 import {Fill} from '#/components/Fill' 36 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 37 38 interface ShouldStartLoadRequest { 39 url: string ··· 148 const {height: winHeight, width: winWidth} = windowDims 149 150 // 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' 29 import {useExternalEmbedsPrefs} from '#/state/preferences' 30 import {EventStopper} from '#/view/com/util/EventStopper' 31 import {atoms as a, useTheme} from '#/alf' ··· 33 import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent' 34 import {Fill} from '#/components/Fill' 35 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 36 + import {IS_NATIVE} from '#/env' 37 38 interface ShouldStartLoadRequest { 39 url: string ··· 148 const {height: winHeight, width: winWidth} = windowDims 149 150 // 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
··· 13 import {HITSLOP_20} from '#/lib/constants' 14 import {clamp} from '#/lib/numbers' 15 import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 16 - import {isWeb} from '#/platform/detection' 17 import {useAutoplayDisabled} from '#/state/preferences' 18 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 19 import {atoms as a, useTheme} from '#/alf' ··· 22 import * as Prompt from '#/components/Prompt' 23 import {Text} from '#/components/Typography' 24 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 25 import {GifView} from '../../../../../modules/expo-bluesky-gif-view' 26 import {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 })
··· 13 import {HITSLOP_20} from '#/lib/constants' 14 import {clamp} from '#/lib/numbers' 15 import {type EmbedPlayerParams} from '#/lib/strings/embed-player' 16 import {useAutoplayDisabled} from '#/state/preferences' 17 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 18 import {atoms as a, useTheme} from '#/alf' ··· 21 import * as Prompt from '#/components/Prompt' 22 import {Text} from '#/components/Typography' 23 import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' 24 + import {IS_WEB} from '#/env' 25 import {GifView} from '../../../../../modules/expo-bluesky-gif-view' 26 import {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
··· 10 import {shareUrl} from '#/lib/sharing' 11 import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' 12 import {toNiceDomain} from '#/lib/strings/url-helpers' 13 - import {isNative} from '#/platform/detection' 14 import {useExternalEmbedsPrefs} from '#/state/preferences' 15 import {atoms as a, useTheme} from '#/alf' 16 import {Divider} from '#/components/Divider' 17 import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 18 import {Link} from '#/components/Link' 19 import {Text} from '#/components/Typography' 20 import {ExternalGif} from './ExternalGif' 21 import {ExternalPlayer} from './ExternalPlayer' 22 import {GifEmbed} from './Gif' ··· 53 }, [playHaptic, onOpen]) 54 55 const onShareExternal = useCallback(() => { 56 - if (link.uri && isNative) { 57 playHaptic('Heavy') 58 shareUrl(link.uri) 59 }
··· 10 import {shareUrl} from '#/lib/sharing' 11 import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' 12 import {toNiceDomain} from '#/lib/strings/url-helpers' 13 import {useExternalEmbedsPrefs} from '#/state/preferences' 14 import {atoms as a, useTheme} from '#/alf' 15 import {Divider} from '#/components/Divider' 16 import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 17 import {Link} from '#/components/Link' 18 import {Text} from '#/components/Typography' 19 + import {IS_NATIVE} from '#/env' 20 import {ExternalGif} from './ExternalGif' 21 import {ExternalPlayer} from './ExternalPlayer' 22 import {GifEmbed} from './Gif' ··· 53 }, [playHaptic, onOpen]) 54 55 const onShareExternal = useCallback(() => { 56 + if (link.uri && IS_NATIVE) { 57 playHaptic('Heavy') 58 shareUrl(link.uri) 59 }
+3 -3
src/components/Post/Embed/VideoEmbed/ActiveVideoWebContext.tsx
··· 8 } from 'react' 9 import {useWindowDimensions} from 'react-native' 10 11 - import {isNative, isWeb} from '#/platform/detection' 12 13 const Context = React.createContext<{ 14 activeViewId: string | null ··· 18 Context.displayName = 'ActiveVideoWebContext' 19 20 export function Provider({children}: {children: React.ReactNode}) { 21 - if (!isWeb) { 22 throw new Error('ActiveVideoWebContext may only be used on web.') 23 } 24 ··· 47 48 const sendViewPosition = useCallback( 49 (viewId: string, y: number) => { 50 - if (isNative) return 51 52 if (viewId === activeViewIdRef.current) { 53 activeViewLocationRef.current = y
··· 8 } from 'react' 9 import {useWindowDimensions} from 'react-native' 10 11 + import {IS_NATIVE, IS_WEB} from '#/env' 12 13 const Context = React.createContext<{ 14 activeViewId: string | null ··· 18 Context.displayName = 'ActiveVideoWebContext' 19 20 export function Provider({children}: {children: React.ReactNode}) { 21 + if (!IS_WEB) { 22 throw new Error('ActiveVideoWebContext may only be used on web.') 23 } 24 ··· 47 48 const sendViewPosition = useCallback( 49 (viewId: string, y: number) => { 50 + if (IS_NATIVE) return 51 52 if (viewId === activeViewIdRef.current) { 53 activeViewLocationRef.current = y
+2 -2
src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
··· 6 7 import {isTouchDevice} from '#/lib/browser' 8 import {clamp} from '#/lib/numbers' 9 - import {isIPhoneWeb} from '#/platform/detection' 10 import { 11 useAutoplayDisabled, 12 useSetSubtitlesEnabled, ··· 28 import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' 29 import {Loader} from '#/components/Loader' 30 import {Text} from '#/components/Typography' 31 import {TimeIndicator} from '../TimeIndicator' 32 import {ControlButton} from './ControlButton' 33 import {Scrubber} from './Scrubber' ··· 400 onEndHover={onVolumeEndHover} 401 drawFocus={drawFocus} 402 /> 403 - {!isIPhoneWeb && ( 404 <ControlButton 405 active={isFullscreen} 406 activeLabel={_(msg`Exit fullscreen`)}
··· 6 7 import {isTouchDevice} from '#/lib/browser' 8 import {clamp} from '#/lib/numbers' 9 import { 10 useAutoplayDisabled, 11 useSetSubtitlesEnabled, ··· 27 import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play' 28 import {Loader} from '#/components/Loader' 29 import {Text} from '#/components/Typography' 30 + import {IS_WEB_MOBILE_IOS} from '#/env' 31 import {TimeIndicator} from '../TimeIndicator' 32 import {ControlButton} from './ControlButton' 33 import {Scrubber} from './Scrubber' ··· 400 onEndHover={onVolumeEndHover} 401 drawFocus={drawFocus} 402 /> 403 + {!IS_WEB_MOBILE_IOS && ( 404 <ControlButton 405 active={isFullscreen} 406 activeLabel={_(msg`Exit fullscreen`)}
+2 -2
src/components/PostControls/ShareMenu/ShareMenuItems.tsx
··· 10 import {shareText, shareUrl} from '#/lib/sharing' 11 import {toShareUrl} from '#/lib/strings/url-helpers' 12 import {logger} from '#/logger' 13 - import {isIOS} from '#/platform/detection' 14 import {useProfileShadow} from '#/state/cache/profile-shadow' 15 import {useSession} from '#/state/session' 16 import * as Toast from '#/view/com/util/Toast' ··· 24 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' 25 import * as Menu from '#/components/Menu' 26 import {useAgeAssurance} from '#/ageAssurance' 27 import {useDevMode} from '#/storage/hooks/dev-mode' 28 import {RecentChats} from './RecentChats' 29 import {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 {
··· 10 import {shareText, shareUrl} from '#/lib/sharing' 11 import {toShareUrl} from '#/lib/strings/url-helpers' 12 import {logger} from '#/logger' 13 import {useProfileShadow} from '#/state/cache/profile-shadow' 14 import {useSession} from '#/state/session' 15 import * as Toast from '#/view/com/util/Toast' ··· 23 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane' 24 import * as Menu from '#/components/Menu' 25 import {useAgeAssurance} from '#/ageAssurance' 26 + import {IS_IOS} from '#/env' 27 import {useDevMode} from '#/storage/hooks/dev-mode' 28 import {RecentChats} from './RecentChats' 29 import {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 {
+2 -2
src/components/PostControls/ShareMenu/ShareMenuItems.web.tsx
··· 9 import {shareText, shareUrl} from '#/lib/sharing' 10 import {toShareUrl} from '#/lib/strings/url-helpers' 11 import {logger} from '#/logger' 12 - import {isWeb} from '#/platform/detection' 13 import {useProfileShadow} from '#/state/cache/profile-shadow' 14 import {useSession} from '#/state/session' 15 import {useBreakpoints} from '#/alf' ··· 22 import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane' 23 import * as Menu from '#/components/Menu' 24 import {useAgeAssurance} from '#/ageAssurance' 25 import {useDevMode} from '#/storage/hooks/dev-mode' 26 import {type ShareMenuItemsProps} from './ShareMenuItems.types' 27 ··· 70 }) 71 } 72 73 - const canEmbed = isWeb && gtMobile && !hideInPWI 74 75 const onShareATURI = () => { 76 shareText(postUri)
··· 9 import {shareText, shareUrl} from '#/lib/sharing' 10 import {toShareUrl} from '#/lib/strings/url-helpers' 11 import {logger} from '#/logger' 12 import {useProfileShadow} from '#/state/cache/profile-shadow' 13 import {useSession} from '#/state/session' 14 import {useBreakpoints} from '#/alf' ··· 21 import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane' 22 import * as Menu from '#/components/Menu' 23 import {useAgeAssurance} from '#/ageAssurance' 24 + import {IS_WEB} from '#/env' 25 import {useDevMode} from '#/storage/hooks/dev-mode' 26 import {type ShareMenuItemsProps} from './ShareMenuItems.types' 27 ··· 70 }) 71 } 72 73 + const canEmbed = IS_WEB && gtMobile && !hideInPWI 74 75 const onShareATURI = () => { 76 shareText(postUri)
+5 -5
src/components/ProgressGuide/FollowDialog.tsx
··· 12 import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 13 import {logEvent} from '#/lib/statsig/statsig' 14 import {logger} from '#/logger' 15 - import {isWeb} from '#/platform/detection' 16 import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 import {useActorSearch} from '#/state/queries/actor-search' 18 import {usePreferencesQuery} from '#/state/queries/preferences' ··· 37 import {boostInterests, InterestTabs} from '#/components/InterestTabs' 38 import * as ProfileCard from '#/components/ProfileCard' 39 import {Text} from '#/components/Typography' 40 import type * as bsky from '#/types/bsky' 41 import {ProgressGuideTask} from './Task' 42 ··· 431 <Trans>Find people to follow</Trans> 432 </Text> 433 {guide && ( 434 - <View style={isWeb && {paddingRight: 36}}> 435 <ProgressGuideTask 436 current={guide.numFollows + 1} 437 total={10 + 1} ··· 440 /> 441 </View> 442 )} 443 - {isWeb ? ( 444 <Button 445 label={_(msg`Close`)} 446 size="small" 447 shape="round" 448 - variant={isWeb ? 'ghost' : 'solid'} 449 color="secondary" 450 style={[ 451 a.absolute, ··· 579 <ProfileCard.Outer> 580 <ProfileCard.Header> 581 <ProfileCard.Avatar 582 - disabledPreview={!isWeb} 583 profile={profile} 584 moderationOpts={moderationOpts} 585 />
··· 12 import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 13 import {logEvent} from '#/lib/statsig/statsig' 14 import {logger} from '#/logger' 15 import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 import {useActorSearch} from '#/state/queries/actor-search' 17 import {usePreferencesQuery} from '#/state/queries/preferences' ··· 36 import {boostInterests, InterestTabs} from '#/components/InterestTabs' 37 import * as ProfileCard from '#/components/ProfileCard' 38 import {Text} from '#/components/Typography' 39 + import {IS_WEB} from '#/env' 40 import type * as bsky from '#/types/bsky' 41 import {ProgressGuideTask} from './Task' 42 ··· 431 <Trans>Find people to follow</Trans> 432 </Text> 433 {guide && ( 434 + <View style={IS_WEB && {paddingRight: 36}}> 435 <ProgressGuideTask 436 current={guide.numFollows + 1} 437 total={10 + 1} ··· 440 /> 441 </View> 442 )} 443 + {IS_WEB ? ( 444 <Button 445 label={_(msg`Close`)} 446 size="small" 447 shape="round" 448 + variant={IS_WEB ? 'ghost' : 'solid'} 449 color="secondary" 450 style={[ 451 a.absolute, ··· 579 <ProfileCard.Outer> 580 <ProfileCard.Header> 581 <ProfileCard.Avatar 582 + disabledPreview={!IS_WEB} 583 profile={profile} 584 moderationOpts={moderationOpts} 585 />
+3 -3
src/components/ProgressGuide/Toast.tsx
··· 11 import {msg} from '@lingui/macro' 12 import {useLingui} from '@lingui/react' 13 14 - import {isWeb} from '#/platform/detection' 15 import {atoms as a, useTheme} from '#/alf' 16 import {Portal} from '#/components/Portal' 17 import {AnimatedCheck, type AnimatedCheckRef} from '../anim/AnimatedCheck' 18 import {Text} from '../Typography' 19 ··· 108 const containerStyle = React.useMemo(() => { 109 let left = 10 110 let right = 10 111 - if (isWeb && winDim.width > 400) { 112 left = right = (winDim.width - 380) / 2 113 } 114 return { 115 - position: isWeb ? 'fixed' : 'absolute', 116 top: 0, 117 left, 118 right,
··· 11 import {msg} from '@lingui/macro' 12 import {useLingui} from '@lingui/react' 13 14 import {atoms as a, useTheme} from '#/alf' 15 import {Portal} from '#/components/Portal' 16 + import {IS_WEB} from '#/env' 17 import {AnimatedCheck, type AnimatedCheckRef} from '../anim/AnimatedCheck' 18 import {Text} from '../Typography' 19 ··· 108 const containerStyle = React.useMemo(() => { 109 let left = 10 110 let right = 10 111 + if (IS_WEB && winDim.width > 400) { 112 left = right = (winDim.width - 380) / 2 113 } 114 return { 115 + position: IS_WEB ? 'fixed' : 'absolute', 116 top: 0, 117 left, 118 right,
+5 -5
src/components/RichTextTag.tsx
··· 6 7 import {type NavigationProp} from '#/lib/routes/types' 8 import {isInvalidHandle} from '#/lib/strings/handles' 9 - import {isNative, isWeb} from '#/platform/detection' 10 import { 11 usePreferencesQuery, 12 useRemoveMutedWordsMutation, ··· 22 } from '#/components/Link' 23 import {Loader} from '#/components/Loader' 24 import * as Menu from '#/components/Menu' 25 26 export function RichTextTag({ 27 tag, ··· 50 const navigation = useNavigation<NavigationProp>() 51 const isCashtag = tag.startsWith('$') 52 const label = isCashtag ? _(msg`Cashtag ${tag}`) : _(msg`Hashtag ${tag}`) 53 - const hint = isNative 54 ? _(msg`Long press to open tag menu for ${isCashtag ? tag : `#${tag}`}`) 55 : _(msg`Click to open tag menu for ${isCashtag ? tag : `#${tag}`}`) 56 ··· 86 }} 87 {...menuProps} 88 onPress={e => { 89 - if (isWeb) { 90 return createStaticClickIfUnmodified(() => { 91 - if (!isNative) { 92 menuProps.onPress() 93 } 94 }).onPress(e) ··· 99 label={label} 100 style={textStyle} 101 emoji> 102 - {isNative ? ( 103 display 104 ) : ( 105 <RNText ref={menuProps.ref}>{display}</RNText>
··· 6 7 import {type NavigationProp} from '#/lib/routes/types' 8 import {isInvalidHandle} from '#/lib/strings/handles' 9 import { 10 usePreferencesQuery, 11 useRemoveMutedWordsMutation, ··· 21 } from '#/components/Link' 22 import {Loader} from '#/components/Loader' 23 import * as Menu from '#/components/Menu' 24 + import {IS_NATIVE, IS_WEB} from '#/env' 25 26 export function RichTextTag({ 27 tag, ··· 50 const navigation = useNavigation<NavigationProp>() 51 const isCashtag = tag.startsWith('$') 52 const label = isCashtag ? _(msg`Cashtag ${tag}`) : _(msg`Hashtag ${tag}`) 53 + const hint = IS_NATIVE 54 ? _(msg`Long press to open tag menu for ${isCashtag ? tag : `#${tag}`}`) 55 : _(msg`Click to open tag menu for ${isCashtag ? tag : `#${tag}`}`) 56 ··· 86 }} 87 {...menuProps} 88 onPress={e => { 89 + if (IS_WEB) { 90 return createStaticClickIfUnmodified(() => { 91 + if (!IS_NATIVE) { 92 menuProps.onPress() 93 } 94 }).onPress(e) ··· 99 label={label} 100 style={textStyle} 101 emoji> 102 + {IS_NATIVE ? ( 103 display 104 ) : ( 105 <RNText ref={menuProps.ref}>{display}</RNText>
+3 -3
src/components/ScreenTransition.tsx
··· 8 } from 'react-native-reanimated' 9 import type React from 'react' 10 11 - import {isWeb} from '#/platform/detection' 12 13 export function ScreenTransition({ 14 direction, ··· 31 32 return ( 33 <Animated.View 34 - entering={isWeb ? webEntering : entering} 35 - exiting={isWeb ? webExiting : exiting} 36 style={style}> 37 {children} 38 </Animated.View>
··· 8 } from 'react-native-reanimated' 9 import type React from 'react' 10 11 + import {IS_WEB} from '#/env' 12 13 export function ScreenTransition({ 14 direction, ··· 31 32 return ( 33 <Animated.View 34 + entering={IS_WEB ? webEntering : entering} 35 + exiting={IS_WEB ? webExiting : exiting} 36 style={style}> 37 {children} 38 </Animated.View>
+1 -1
src/components/Select/index.tsx
··· 82 83 if (typeof children === 'function') { 84 return children({ 85 - isNative: true, 86 control, 87 state: { 88 hovered: false,
··· 82 83 if (typeof children === 'function') { 84 return children({ 85 + IS_NATIVE: true, 86 control, 87 state: { 88 hovered: false,
+1 -1
src/components/Select/index.web.tsx
··· 68 <RadixTriggerPassThrough> 69 {props => 70 children({ 71 - isNative: false, 72 state: { 73 hovered, 74 focused,
··· 68 <RadixTriggerPassThrough> 69 {props => 70 children({ 71 + IS_NATIVE: false, 72 state: { 73 hovered, 74 focused,
+2 -2
src/components/Select/types.ts
··· 65 66 export type TriggerChildProps = 67 | { 68 - isNative: true 69 control: DialogControlProps 70 state: { 71 /** ··· 92 } 93 } 94 | { 95 - isNative: false 96 state: { 97 hovered: boolean 98 focused: boolean
··· 65 66 export type TriggerChildProps = 67 | { 68 + IS_NATIVE: true 69 control: DialogControlProps 70 state: { 71 /** ··· 92 } 93 } 94 | { 95 + IS_NATIVE: false 96 state: { 97 hovered: boolean 98 focused: boolean
+3 -3
src/components/StarterPack/Main/FeedsList.tsx
··· 3 import {type AppBskyFeedDefs} from '@atproto/api' 4 5 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 6 - import {isNative, isWeb} from '#/platform/detection' 7 import {List, type ListRef} from '#/view/com/util/List' 8 import {type SectionRef} from '#/screens/Profile/Sections/types' 9 import {atoms as a, useTheme} from '#/alf' 10 import * as FeedCard from '#/components/FeedCard' 11 12 function keyExtractor(item: AppBskyFeedDefs.GeneratorView) { 13 return item.uri ··· 27 28 const onScrollToTop = useCallback(() => { 29 scrollElRef.current?.scrollToOffset({ 30 - animated: isNative, 31 offset: -headerHeight, 32 }) 33 }, [scrollElRef, headerHeight]) ··· 44 <View 45 style={[ 46 a.p_lg, 47 - (isWeb || index !== 0) && a.border_t, 48 t.atoms.border_contrast_low, 49 ]}> 50 <FeedCard.Default view={item} />
··· 3 import {type AppBskyFeedDefs} from '@atproto/api' 4 5 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 6 import {List, type ListRef} from '#/view/com/util/List' 7 import {type SectionRef} from '#/screens/Profile/Sections/types' 8 import {atoms as a, useTheme} from '#/alf' 9 import * as FeedCard from '#/components/FeedCard' 10 + import {IS_NATIVE, IS_WEB} from '#/env' 11 12 function keyExtractor(item: AppBskyFeedDefs.GeneratorView) { 13 return item.uri ··· 27 28 const onScrollToTop = useCallback(() => { 29 scrollElRef.current?.scrollToOffset({ 30 + animated: IS_NATIVE, 31 offset: -headerHeight, 32 }) 33 }, [scrollElRef, headerHeight]) ··· 44 <View 45 style={[ 46 a.p_lg, 47 + (IS_WEB || index !== 0) && a.border_t, 48 t.atoms.border_contrast_low, 49 ]}> 50 <FeedCard.Default view={item} />
+2 -2
src/components/StarterPack/Main/PostsList.tsx
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 - import {isNative} from '#/platform/detection' 7 import {type FeedDescriptor} from '#/state/queries/post-feed' 8 import {PostFeed} from '#/view/com/posts/PostFeed' 9 import {EmptyState} from '#/view/com/util/EmptyState' 10 import {type ListRef} from '#/view/com/util/List' 11 import {type SectionRef} from '#/screens/Profile/Sections/types' 12 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 13 14 interface ProfilesListProps { 15 listUri: string ··· 24 25 const onScrollToTop = useCallback(() => { 26 scrollElRef.current?.scrollToOffset({ 27 - animated: isNative, 28 offset: -headerHeight, 29 }) 30 }, [scrollElRef, headerHeight])
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {type FeedDescriptor} from '#/state/queries/post-feed' 7 import {PostFeed} from '#/view/com/posts/PostFeed' 8 import {EmptyState} from '#/view/com/util/EmptyState' 9 import {type ListRef} from '#/view/com/util/List' 10 import {type SectionRef} from '#/screens/Profile/Sections/types' 11 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 12 + import {IS_NATIVE} from '#/env' 13 14 interface ProfilesListProps { 15 listUri: string ··· 24 25 const onScrollToTop = useCallback(() => { 26 scrollElRef.current?.scrollToOffset({ 27 + animated: IS_NATIVE, 28 offset: -headerHeight, 29 }) 30 }, [scrollElRef, headerHeight])
+3 -3
src/components/StarterPack/Main/ProfilesList.tsx
··· 14 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 15 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 16 import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' 17 - import {isNative, isWeb} from '#/platform/detection' 18 import {useAllListMembersQuery} from '#/state/queries/list-members' 19 import {useSession} from '#/state/session' 20 import {List, type ListRef} from '#/view/com/util/List' ··· 22 import {atoms as a, useTheme} from '#/alf' 23 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 24 import {Default as ProfileCard} from '#/components/ProfileCard' 25 26 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { 27 return `${item.did}-${index}` ··· 75 } 76 const onScrollToTop = useCallback(() => { 77 scrollElRef.current?.scrollToOffset({ 78 - animated: isNative, 79 offset: -headerHeight, 80 }) 81 }, [scrollElRef, headerHeight]) ··· 93 style={[ 94 a.p_lg, 95 t.atoms.border_contrast_low, 96 - (isWeb || index !== 0) && a.border_t, 97 ]}> 98 <ProfileCard 99 profile={item}
··· 14 import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' 15 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 16 import {isBlockedOrBlocking} from '#/lib/moderation/blocked-and-muted' 17 import {useAllListMembersQuery} from '#/state/queries/list-members' 18 import {useSession} from '#/state/session' 19 import {List, type ListRef} from '#/view/com/util/List' ··· 21 import {atoms as a, useTheme} from '#/alf' 22 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 23 import {Default as ProfileCard} from '#/components/ProfileCard' 24 + import {IS_NATIVE, IS_WEB} from '#/env' 25 26 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { 27 return `${item.did}-${index}` ··· 75 } 76 const onScrollToTop = useCallback(() => { 77 scrollElRef.current?.scrollToOffset({ 78 + animated: IS_NATIVE, 79 offset: -headerHeight, 80 }) 81 }, [scrollElRef, headerHeight]) ··· 93 style={[ 94 a.p_lg, 95 t.atoms.border_contrast_low, 96 + (IS_WEB || index !== 0) && a.border_t, 97 ]}> 98 <ProfileCard 99 profile={item}
+2 -2
src/components/StarterPack/ProfileStarterPacks.tsx
··· 19 import {type NavigationProp} from '#/lib/routes/types' 20 import {parseStarterPackUri} from '#/lib/strings/starter-pack' 21 import {logger} from '#/logger' 22 - import {isIOS} from '#/platform/detection' 23 import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' 24 import { 25 EmptyState, ··· 36 import * as Prompt from '#/components/Prompt' 37 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 38 import {Text} from '#/components/Typography' 39 40 interface SectionRef { 41 scrollToTop: () => void ··· 136 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 137 138 useEffect(() => { 139 - if (isIOS && enabled && scrollElRef.current) { 140 const nativeTag = findNodeHandle(scrollElRef.current) 141 setScrollViewTag(nativeTag) 142 }
··· 19 import {type NavigationProp} from '#/lib/routes/types' 20 import {parseStarterPackUri} from '#/lib/strings/starter-pack' 21 import {logger} from '#/logger' 22 import {useActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' 23 import { 24 EmptyState, ··· 35 import * as Prompt from '#/components/Prompt' 36 import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard' 37 import {Text} from '#/components/Typography' 38 + import {IS_IOS} from '#/env' 39 40 interface SectionRef { 41 scrollToTop: () => void ··· 136 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage]) 137 138 useEffect(() => { 139 + if (IS_IOS && enabled && scrollElRef.current) { 140 const nativeTag = findNodeHandle(scrollElRef.current) 141 setScrollViewTag(nativeTag) 142 }
+6 -6
src/components/StarterPack/QrCode.tsx
··· 6 import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 7 import {Trans} from '@lingui/macro' 8 9 - import {isWeb} from '#/platform/detection' 10 import {Logo} from '#/view/icons/Logo' 11 import {Logotype} from '#/view/icons/Logotype' 12 import {useTheme} from '#/alf' 13 import {atoms as a} from '#/alf' 14 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 15 import {Text} from '#/components/Typography' 16 import * as bsky from '#/types/bsky' 17 18 const LazyViewShot = lazy( ··· 121 return ( 122 <View style={{position: 'relative'}}> 123 {/* An SVG version of the logo is placed on top of normal `QRCode` `logo` prop, since the PNG fails to load before the export completes on web. */} 124 - {isWeb && logoArea && ( 125 <View 126 style={{ 127 position: 'absolute', ··· 139 a.rounded_sm, 140 {height: 225, width: 225, backgroundColor: '#f3f3f3'}, 141 ]} 142 - pieceSize={isWeb ? 8 : 6} 143 padding={20} 144 - pieceBorderRadius={isWeb ? 4.5 : 3.5} 145 outerEyesOptions={{ 146 topLeft: { 147 borderRadius: [12, 12, 0, 12], ··· 159 innerEyesOptions={{borderRadius: 3}} 160 logo={{ 161 href: require('../../../assets/logo.png'), 162 - ...(isWeb && { 163 onChange: onLogoAreaChange, 164 padding: 28, 165 }), 166 - ...(!isWeb && { 167 padding: 2, 168 scale: 0.95, 169 }),
··· 6 import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' 7 import {Trans} from '@lingui/macro' 8 9 import {Logo} from '#/view/icons/Logo' 10 import {Logotype} from '#/view/icons/Logotype' 11 import {useTheme} from '#/alf' 12 import {atoms as a} from '#/alf' 13 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 14 import {Text} from '#/components/Typography' 15 + import {IS_WEB} from '#/env' 16 import * as bsky from '#/types/bsky' 17 18 const LazyViewShot = lazy( ··· 121 return ( 122 <View style={{position: 'relative'}}> 123 {/* An SVG version of the logo is placed on top of normal `QRCode` `logo` prop, since the PNG fails to load before the export completes on web. */} 124 + {IS_WEB && logoArea && ( 125 <View 126 style={{ 127 position: 'absolute', ··· 139 a.rounded_sm, 140 {height: 225, width: 225, backgroundColor: '#f3f3f3'}, 141 ]} 142 + pieceSize={IS_WEB ? 8 : 6} 143 padding={20} 144 + pieceBorderRadius={IS_WEB ? 4.5 : 3.5} 145 outerEyesOptions={{ 146 topLeft: { 147 borderRadius: [12, 12, 0, 12], ··· 159 innerEyesOptions={{borderRadius: 3}} 160 logo={{ 161 href: require('../../../assets/logo.png'), 162 + ...(IS_WEB && { 163 onChange: onLogoAreaChange, 164 padding: 28, 165 }), 166 + ...(!IS_WEB && { 167 padding: 2, 168 scale: 0.95, 169 }),
+6 -6
src/components/StarterPack/QrCodeDialog.tsx
··· 9 import {useLingui} from '@lingui/react' 10 11 import {logger} from '#/logger' 12 - import {isNative, isWeb} from '#/platform/detection' 13 import {atoms as a, useBreakpoints} from '#/alf' 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import * as Dialog from '#/components/Dialog' ··· 20 import {Loader} from '#/components/Loader' 21 import {QrCode} from '#/components/StarterPack/QrCode' 22 import * as Toast from '#/components/Toast' 23 import * as bsky from '#/types/bsky' 24 25 export function QrCodeDialog({ ··· 56 57 const onSavePress = async () => { 58 ref.current?.capture?.().then(async (uri: string) => { 59 - if (isNative) { 60 const res = await requestMediaLibraryPermissionsAsync() 61 62 if (!res.granted) { ··· 111 }) 112 setIsSaveProcessing(false) 113 Toast.show( 114 - isWeb 115 ? _(msg`QR code has been downloaded!`) 116 : _(msg`QR code saved to your camera roll!`), 117 ) ··· 178 label={_(msg`Copy QR code`)} 179 color="primary_subtle" 180 size="large" 181 - onPress={isWeb ? onCopyPress : onSharePress}> 182 <ButtonIcon 183 icon={ 184 isCopyProcessing 185 ? Loader 186 - : isWeb 187 ? ChainLinkIcon 188 : ShareIcon 189 } 190 /> 191 <ButtonText> 192 - {isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>} 193 </ButtonText> 194 </Button> 195 <Button
··· 9 import {useLingui} from '@lingui/react' 10 11 import {logger} from '#/logger' 12 import {atoms as a, useBreakpoints} from '#/alf' 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 import * as Dialog from '#/components/Dialog' ··· 19 import {Loader} from '#/components/Loader' 20 import {QrCode} from '#/components/StarterPack/QrCode' 21 import * as Toast from '#/components/Toast' 22 + import {IS_NATIVE, IS_WEB} from '#/env' 23 import * as bsky from '#/types/bsky' 24 25 export function QrCodeDialog({ ··· 56 57 const onSavePress = async () => { 58 ref.current?.capture?.().then(async (uri: string) => { 59 + if (IS_NATIVE) { 60 const res = await requestMediaLibraryPermissionsAsync() 61 62 if (!res.granted) { ··· 111 }) 112 setIsSaveProcessing(false) 113 Toast.show( 114 + IS_WEB 115 ? _(msg`QR code has been downloaded!`) 116 : _(msg`QR code saved to your camera roll!`), 117 ) ··· 178 label={_(msg`Copy QR code`)} 179 color="primary_subtle" 180 size="large" 181 + onPress={IS_WEB ? onCopyPress : onSharePress}> 182 <ButtonIcon 183 icon={ 184 isCopyProcessing 185 ? Loader 186 + : IS_WEB 187 ? ChainLinkIcon 188 : ShareIcon 189 } 190 /> 191 <ButtonText> 192 + {IS_WEB ? <Trans>Copy</Trans> : <Trans>Share</Trans>} 193 </ButtonText> 194 </Button> 195 <Button
+8 -4
src/components/StarterPack/ShareDialog.tsx
··· 8 import {shareUrl} from '#/lib/sharing' 9 import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 10 import {logger} from '#/logger' 11 - import {isNative, isWeb} from '#/platform/detection' 12 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 import {type DialogControlProps} from '#/components/Dialog' ··· 18 import {QrCode_Stroke2_Corner0_Rounded as QrCodeIcon} from '#/components/icons/QrCode' 19 import {Loader} from '#/components/Loader' 20 import {Text} from '#/components/Typography' 21 22 interface Props { 23 starterPack: AppBskyGraphDefs.StarterPackView ··· 110 ], 111 ]}> 112 <Button 113 - label={isWeb ? _(msg`Copy link`) : _(msg`Share link`)} 114 color="primary_subtle" 115 size="large" 116 onPress={onShareLink}> 117 <ButtonIcon icon={ChainLinkIcon} /> 118 <ButtonText> 119 - {isWeb ? <Trans>Copy Link</Trans> : <Trans>Share link</Trans>} 120 </ButtonText> 121 </Button> 122 <Button ··· 133 <Trans>Share QR code</Trans> 134 </ButtonText> 135 </Button> 136 - {isNative && ( 137 <Button 138 label={_(msg`Save image`)} 139 color="secondary"
··· 8 import {shareUrl} from '#/lib/sharing' 9 import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 10 import {logger} from '#/logger' 11 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 12 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 13 import {type DialogControlProps} from '#/components/Dialog' ··· 17 import {QrCode_Stroke2_Corner0_Rounded as QrCodeIcon} from '#/components/icons/QrCode' 18 import {Loader} from '#/components/Loader' 19 import {Text} from '#/components/Typography' 20 + import {IS_NATIVE, IS_WEB} from '#/env' 21 22 interface Props { 23 starterPack: AppBskyGraphDefs.StarterPackView ··· 110 ], 111 ]}> 112 <Button 113 + label={IS_WEB ? _(msg`Copy link`) : _(msg`Share link`)} 114 color="primary_subtle" 115 size="large" 116 onPress={onShareLink}> 117 <ButtonIcon icon={ChainLinkIcon} /> 118 <ButtonText> 119 + {IS_WEB ? ( 120 + <Trans>Copy Link</Trans> 121 + ) : ( 122 + <Trans>Share link</Trans> 123 + )} 124 </ButtonText> 125 </Button> 126 <Button ··· 137 <Trans>Share QR code</Trans> 138 </ButtonText> 139 </Button> 140 + {IS_NATIVE && ( 141 <Button 142 label={_(msg`Save image`)} 143 color="secondary"
+3 -3
src/components/StarterPack/Wizard/WizardEditListDialog.tsx
··· 10 import {useLingui} from '@lingui/react' 11 12 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 13 - import {isWeb} from '#/platform/detection' 14 import {type ListMethods} from '#/view/com/util/List' 15 import { 16 type WizardAction, ··· 24 WizardProfileCard, 25 } from '#/components/StarterPack/Wizard/WizardListCard' 26 import {Text} from '#/components/Typography' 27 28 function keyExtractor( 29 item: AppBskyActorDefs.ProfileViewBasic | AppBskyFeedDefs.GeneratorView, ··· 95 a.mb_sm, 96 t.atoms.bg, 97 t.atoms.border_contrast_medium, 98 - isWeb 99 ? [ 100 a.align_center, 101 { ··· 113 )} 114 </Text> 115 <View style={{width: 60}}> 116 - {isWeb && ( 117 <Button 118 label={_(msg`Close`)} 119 variant="ghost"
··· 10 import {useLingui} from '@lingui/react' 11 12 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 13 import {type ListMethods} from '#/view/com/util/List' 14 import { 15 type WizardAction, ··· 23 WizardProfileCard, 24 } from '#/components/StarterPack/Wizard/WizardListCard' 25 import {Text} from '#/components/Typography' 26 + import {IS_WEB} from '#/env' 27 28 function keyExtractor( 29 item: AppBskyActorDefs.ProfileViewBasic | AppBskyFeedDefs.GeneratorView, ··· 95 a.mb_sm, 96 t.atoms.bg, 97 t.atoms.border_contrast_medium, 98 + IS_WEB 99 ? [ 100 a.align_center, 101 { ··· 113 )} 114 </Text> 115 <View style={{width: 60}}> 116 + {IS_WEB && ( 117 <Button 118 label={_(msg`Close`)} 119 variant="ghost"
+3 -3
src/components/SubtleHover.tsx
··· 1 import {View} from 'react-native' 2 3 import {isTouchDevice} from '#/lib/browser' 4 - import {isNative, isWeb} from '#/platform/detection' 5 import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' 6 7 export function SubtleHover({ 8 style, ··· 39 /> 40 ) 41 42 - if (isWeb && web) { 43 return isTouchDevice ? null : el 44 - } else if (isNative && native) { 45 return el 46 } 47
··· 1 import {View} from 'react-native' 2 3 import {isTouchDevice} from '#/lib/browser' 4 import {atoms as a, useTheme, type ViewStyleProp} from '#/alf' 5 + import {IS_NATIVE, IS_WEB} from '#/env' 6 7 export function SubtleHover({ 8 style, ··· 39 /> 40 ) 41 42 + 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
··· 18 import {HITSLOP_10} from '#/lib/constants' 19 import {makeListLink, makeProfileLink} from '#/lib/routes/links' 20 import {logger} from '#/logger' 21 - import {isNative} from '#/platform/detection' 22 import { 23 type ThreadgateAllowUISetting, 24 threadgateViewToAllowUISetting, ··· 37 import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group' 38 import {InlineLinkText} from '#/components/Link' 39 import {Text} from '#/components/Typography' 40 import * as bsky from '#/types/bsky' 41 42 interface WhoCanReplyProps { ··· 86 : _(msg`Some people can reply`) 87 88 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()}
··· 18 import {HITSLOP_10} from '#/lib/constants' 19 import {makeListLink, makeProfileLink} from '#/lib/routes/links' 20 import {logger} from '#/logger' 21 import { 22 type ThreadgateAllowUISetting, 23 threadgateViewToAllowUISetting, ··· 36 import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group' 37 import {InlineLinkText} from '#/components/Link' 38 import {Text} from '#/components/Typography' 39 + import {IS_NATIVE} from '#/env' 40 import * as bsky from '#/types/bsky' 41 42 interface WhoCanReplyProps { ··· 86 : _(msg`Some people can reply`) 87 88 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()}
+2 -2
src/components/activity-notifications/SubscribeProfileDialog.tsx
··· 18 import {cleanError} from '#/lib/strings/errors' 19 import {sanitizeHandle} from '#/lib/strings/handles' 20 import {logger} from '#/logger' 21 - import {isWeb} from '#/platform/detection' 22 import {updateProfileShadow} from '#/state/cache/profile-shadow' 23 import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' 24 import {useAgent} from '#/state/session' ··· 37 import {Loader} from '#/components/Loader' 38 import * as ProfileCard from '#/components/ProfileCard' 39 import {Text} from '#/components/Typography' 40 import type * as bsky from '#/types/bsky' 41 42 export 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',
··· 18 import {cleanError} from '#/lib/strings/errors' 19 import {sanitizeHandle} from '#/lib/strings/handles' 20 import {logger} from '#/logger' 21 import {updateProfileShadow} from '#/state/cache/profile-shadow' 22 import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' 23 import {useAgent} from '#/state/session' ··· 36 import {Loader} from '#/components/Loader' 37 import * as ProfileCard from '#/components/ProfileCard' 38 import {Text} from '#/components/Typography' 39 + import {IS_WEB} from '#/env' 40 import type * as bsky from '#/types/bsky' 41 42 export 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',
+2 -2
src/components/ageAssurance/AgeAssuranceAccountCard.tsx
··· 3 import {useLingui} from '@lingui/react' 4 5 import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 6 - import {isNative} from '#/platform/detection' 7 import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' 8 import {Admonition} from '#/components/Admonition' 9 import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' ··· 23 import {Text} from '#/components/Typography' 24 import {logger, useAgeAssurance} from '#/ageAssurance' 25 import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' 26 import {useDeviceGeolocationApi} from '#/geolocation' 27 28 export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { ··· 86 <View style={[a.pb_md, a.gap_xs]}> 87 <Text style={[a.text_sm, a.leading_snug]}>{copy.notice}</Text> 88 89 - {isNative && ( 90 <> 91 <Text style={[a.text_sm, a.leading_snug]}> 92 <Trans>
··· 3 import {useLingui} from '@lingui/react' 4 5 import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 6 import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' 7 import {Admonition} from '#/components/Admonition' 8 import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' ··· 22 import {Text} from '#/components/Typography' 23 import {logger, useAgeAssurance} from '#/ageAssurance' 24 import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' 25 + import {IS_NATIVE} from '#/env' 26 import {useDeviceGeolocationApi} from '#/geolocation' 27 28 export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { ··· 86 <View style={[a.pb_md, a.gap_xs]}> 87 <Text style={[a.text_sm, a.leading_snug]}>{copy.notice}</Text> 88 89 + {IS_NATIVE && ( 90 <> 91 <Text style={[a.text_sm, a.leading_snug]}> 92 <Trans>
+3 -3
src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx
··· 5 6 import {retry} from '#/lib/async/retry' 7 import {wait} from '#/lib/async/wait' 8 - import {isNative} from '#/platform/detection' 9 import {useAgent} from '#/state/session' 10 import {atoms as a, useTheme, web} from '#/alf' 11 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' ··· 18 import {Text} from '#/components/Typography' 19 import {refetchAgeAssuranceServerState} from '#/ageAssurance' 20 import {logger} from '#/ageAssurance' 21 22 export type AgeAssuranceRedirectDialogState = { 23 result: 'success' | 'unknown' ··· 166 </Trans> 167 </Text> 168 169 - {isNative && ( 170 <View style={[a.w_full, a.pt_lg]}> 171 <Button 172 label={_(msg`Close`)} ··· 225 )} 226 </Text> 227 228 - {error && isNative && ( 229 <View style={[a.w_full, a.pt_lg]}> 230 <Button 231 label={_(msg`Close`)}
··· 5 6 import {retry} from '#/lib/async/retry' 7 import {wait} from '#/lib/async/wait' 8 import {useAgent} from '#/state/session' 9 import {atoms as a, useTheme, web} from '#/alf' 10 import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' ··· 17 import {Text} from '#/components/Typography' 18 import {refetchAgeAssuranceServerState} from '#/ageAssurance' 19 import {logger} from '#/ageAssurance' 20 + import {IS_NATIVE} from '#/env' 21 22 export type AgeAssuranceRedirectDialogState = { 23 result: 'success' | 'unknown' ··· 166 </Trans> 167 </Text> 168 169 + {IS_NATIVE && ( 170 <View style={[a.w_full, a.pt_lg]}> 171 <Button 172 label={_(msg`Close`)} ··· 225 )} 226 </Text> 227 228 + {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
··· 8 import {HITSLOP_10} from '#/lib/constants' 9 import {useGate} from '#/lib/statsig/statsig' 10 import {logger} from '#/logger' 11 - import {isWeb} from '#/platform/detection' 12 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' 13 import {atoms as a, useTheme} from '#/alf' 14 import {Button} from '#/components/Button' 15 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 16 import {Text} from '#/components/Typography' 17 import {Link} from '../Link' 18 import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist' 19 ··· 92 const gate = useGate() 93 94 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
··· 8 import {HITSLOP_10} from '#/lib/constants' 9 import {useGate} from '#/lib/statsig/statsig' 10 import {logger} from '#/logger' 11 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' 12 import {atoms as a, useTheme} from '#/alf' 13 import {Button} from '#/components/Button' 14 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 15 import {Text} from '#/components/Typography' 16 + import {IS_WEB} from '#/env' 17 import {Link} from '../Link' 18 import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist' 19 ··· 92 const gate = useGate() 93 94 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
··· 9 import {useLingui} from '@lingui/react' 10 11 import {mergeRefs} from '#/lib/merge-refs' 12 - import {isAndroid, isIOS} from '#/platform/detection' 13 import {atoms as a, ios, platform, useTheme} from '#/alf' 14 import {useInteractionState} from '#/components/hooks/useInteractionState' 15 import {Text} from '#/components/Typography' 16 17 export 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>
··· 9 import {useLingui} from '@lingui/react' 10 11 import {mergeRefs} from '#/lib/merge-refs' 12 import {atoms as a, ios, platform, useTheme} from '#/alf' 13 import {useInteractionState} from '#/components/hooks/useInteractionState' 14 import {Text} from '#/components/Typography' 15 + import {IS_ANDROID, IS_IOS} from '#/env' 16 17 export 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>
+3 -3
src/components/dialogs/BirthDateSettings.tsx
··· 7 import {isAppPassword} from '#/lib/jwt' 8 import {getAge, getDateAgo} from '#/lib/strings/time' 9 import {logger} from '#/logger' 10 - import {isIOS, isWeb} from '#/platform/detection' 11 import { 12 useBirthdateMutation, 13 useIsBirthdateUpdateAllowed, ··· 26 import {SimpleInlineLinkText} from '#/components/Link' 27 import {Loader} from '#/components/Loader' 28 import {Span, Text} from '#/components/Typography' 29 30 export function BirthDateSettingsDialog({ 31 control, ··· 154 155 return ( 156 <View style={a.gap_lg} testID="birthDateSettingsDialog"> 157 - <View style={isIOS && [a.w_full, a.align_center]}> 158 <DateField 159 testID="birthdayInput" 160 value={date} ··· 191 <ErrorMessage message={errorMessage} style={[a.rounded_sm]} /> 192 ) : undefined} 193 194 - <View style={isWeb && [a.flex_row, a.justify_end]}> 195 <Button 196 label={hasChanged ? _(msg`Save birthdate`) : _(msg`Done`)} 197 size="large"
··· 7 import {isAppPassword} from '#/lib/jwt' 8 import {getAge, getDateAgo} from '#/lib/strings/time' 9 import {logger} from '#/logger' 10 import { 11 useBirthdateMutation, 12 useIsBirthdateUpdateAllowed, ··· 25 import {SimpleInlineLinkText} from '#/components/Link' 26 import {Loader} from '#/components/Loader' 27 import {Span, Text} from '#/components/Typography' 28 + import {IS_IOS, IS_WEB} from '#/env' 29 30 export function BirthDateSettingsDialog({ 31 control, ··· 154 155 return ( 156 <View style={a.gap_lg} testID="birthDateSettingsDialog"> 157 + <View style={IS_IOS && [a.w_full, a.align_center]}> 158 <DateField 159 testID="birthdayInput" 160 value={date} ··· 191 <ErrorMessage message={errorMessage} style={[a.rounded_sm]} /> 192 ) : undefined} 193 194 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 195 <Button 196 label={hasChanged ? _(msg`Save birthdate`) : _(msg`Done`)} 197 size="large"
+4 -4
src/components/dialogs/DeviceLocationRequestDialog.tsx
··· 6 import {wait} from '#/lib/async/wait' 7 import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError' 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 10 import {atoms as a, useTheme, web} from '#/alf' 11 import {Admonition} from '#/components/Admonition' 12 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 14 import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation' 15 import {Loader} from '#/components/Loader' 16 import {Text} from '#/components/Typography' 17 import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation' 18 19 export 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 )} 149 150 - {!isWeb && ( 151 <Button 152 label={_(msg`Cancel`)} 153 onPress={() => close()} 154 - size={isWeb ? 'small' : 'large'} 155 color="secondary"> 156 <ButtonText> 157 <Trans>Cancel</Trans>
··· 6 import {wait} from '#/lib/async/wait' 7 import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError' 8 import {logger} from '#/logger' 9 import {atoms as a, useTheme, web} from '#/alf' 10 import {Admonition} from '#/components/Admonition' 11 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 13 import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation' 14 import {Loader} from '#/components/Loader' 15 import {Text} from '#/components/Typography' 16 + import {IS_WEB} from '#/env' 17 import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation' 18 19 export 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 )} 149 150 + {!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
··· 13 14 import {logEvent} from '#/lib/statsig/statsig' 15 import {cleanError} from '#/lib/strings/errors' 16 - import {isWeb} from '#/platform/detection' 17 import { 18 type Gif, 19 tenorUrlToBskyGifUrl, ··· 31 import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 32 import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass' 33 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 34 35 export 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 )} 163 164 - <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}> 165 <TextField.Icon icon={Search} /> 166 <TextField.Input 167 label={_(msg`Search GIFs`)}
··· 13 14 import {logEvent} from '#/lib/statsig/statsig' 15 import {cleanError} from '#/lib/strings/errors' 16 import { 17 type Gif, 18 tenorUrlToBskyGifUrl, ··· 30 import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 31 import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass' 32 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 33 + import {IS_WEB} from '#/env' 34 35 export 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 )} 163 164 + <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
··· 4 import {useLingui} from '@lingui/react' 5 6 import {useOpenLink} from '#/lib/hooks/useOpenLink' 7 - import {isWeb} from '#/platform/detection' 8 import {useSetInAppBrowser} from '#/state/preferences/in-app-browser' 9 import {atoms as a, useTheme} from '#/alf' 10 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 11 import * as Dialog from '#/components/Dialog' 12 import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight' 13 import {Text} from '#/components/Typography' 14 import {useGlobalDialogsControlContext} from './Context' 15 16 export function InAppBrowserConsentDialog() { 17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext() 18 19 - if (isWeb) return null 20 21 return ( 22 <Dialog.Outer
··· 4 import {useLingui} from '@lingui/react' 5 6 import {useOpenLink} from '#/lib/hooks/useOpenLink' 7 import {useSetInAppBrowser} from '#/state/preferences/in-app-browser' 8 import {atoms as a, useTheme} from '#/alf' 9 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 10 import * as Dialog from '#/components/Dialog' 11 import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight' 12 import {Text} from '#/components/Typography' 13 + import {IS_WEB} from '#/env' 14 import {useGlobalDialogsControlContext} from './Context' 15 16 export function InAppBrowserConsentDialog() { 17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext() 18 19 + if (IS_WEB) return null 20 21 return ( 22 <Dialog.Outer
+2 -2
src/components/dialogs/MutedWords.tsx
··· 5 import {useLingui} from '@lingui/react' 6 7 import {logger} from '#/logger' 8 - import {isNative} from '#/platform/detection' 9 import { 10 usePreferencesQuery, 11 useRemoveMutedWordMutation, ··· 32 import {Loader} from '#/components/Loader' 33 import * as Prompt from '#/components/Prompt' 34 import {Text} from '#/components/Typography' 35 36 const ONE_DAY = 24 * 60 * 60 * 1000 37 ··· 406 )} 407 </View> 408 409 - {isNative && <View style={{height: 20}} />} 410 </View> 411 412 <Dialog.Close />
··· 5 import {useLingui} from '@lingui/react' 6 7 import {logger} from '#/logger' 8 import { 9 usePreferencesQuery, 10 useRemoveMutedWordMutation, ··· 31 import {Loader} from '#/components/Loader' 32 import * as Prompt from '#/components/Prompt' 33 import {Text} from '#/components/Typography' 34 + import {IS_NATIVE} from '#/env' 35 36 const ONE_DAY = 24 * 60 * 60 * 1000 37 ··· 406 )} 407 </View> 408 409 + {IS_NATIVE && <View style={{height: 20}} />} 410 </View> 411 412 <Dialog.Close />
+2 -2
src/components/dialogs/PostInteractionSettingsDialog.tsx
··· 12 import {useHaptics} from '#/lib/haptics' 13 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 14 import {logger} from '#/logger' 15 - import {isIOS} from '#/platform/detection' 16 import {STALE} from '#/state/queries' 17 import {useMyListsQuery} from '#/state/queries/my-lists' 18 import {useGetPost} from '#/state/queries/post' ··· 52 import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote' 53 import {Loader} from '#/components/Loader' 54 import {Text} from '#/components/Typography' 55 56 export type PostInteractionSettingsFormProps = { 57 canSave?: boolean ··· 531 hitSlop={0} 532 onPress={() => { 533 playHaptic('Light') 534 - if (isIOS && !showLists) { 535 LayoutAnimation.configureNext({ 536 ...LayoutAnimation.Presets.linear, 537 duration: 175,
··· 12 import {useHaptics} from '#/lib/haptics' 13 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 14 import {logger} from '#/logger' 15 import {STALE} from '#/state/queries' 16 import {useMyListsQuery} from '#/state/queries/my-lists' 17 import {useGetPost} from '#/state/queries/post' ··· 51 import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote' 52 import {Loader} from '#/components/Loader' 53 import {Text} from '#/components/Typography' 54 + import {IS_IOS} from '#/env' 55 56 export type PostInteractionSettingsFormProps = { 57 canSave?: boolean ··· 531 hitSlop={0} 532 onPress={() => { 533 playHaptic('Light') 534 + if (IS_IOS && !showLists) { 535 LayoutAnimation.configureNext({ 536 ...LayoutAnimation.Presets.linear, 537 duration: 175,
+4 -4
src/components/dialogs/SearchablePeopleList.tsx
··· 13 14 import {sanitizeDisplayName} from '#/lib/strings/display-names' 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 - import {isWeb} from '#/platform/detection' 17 import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 19 import {useListConvosQuery} from '#/state/queries/messages/list-conversations' ··· 29 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 30 import * as ProfileCard from '#/components/ProfileCard' 31 import {Text} from '#/components/Typography' 32 import type * as bsky from '#/types/bsky' 33 34 export type ProfileItem = { ··· 254 ) 255 256 useLayoutEffect(() => { 257 - if (isWeb) { 258 setImmediate(() => { 259 inputRef?.current?.focus() 260 }) ··· 290 ]}> 291 {title} 292 </Text> 293 - {isWeb ? ( 294 <Button 295 label={_(msg`Close`)} 296 size="small" 297 shape="round" 298 - variant={isWeb ? 'ghost' : 'solid'} 299 color="secondary" 300 style={[ 301 a.absolute,
··· 13 14 import {sanitizeDisplayName} from '#/lib/strings/display-names' 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 import {useModerationOpts} from '#/state/preferences/moderation-opts' 17 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 18 import {useListConvosQuery} from '#/state/queries/messages/list-conversations' ··· 28 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 29 import * as ProfileCard from '#/components/ProfileCard' 30 import {Text} from '#/components/Typography' 31 + import {IS_WEB} from '#/env' 32 import type * as bsky from '#/types/bsky' 33 34 export type ProfileItem = { ··· 254 ) 255 256 useLayoutEffect(() => { 257 + if (IS_WEB) { 258 setImmediate(() => { 259 inputRef?.current?.focus() 260 }) ··· 290 ]}> 291 {title} 292 </Text> 293 + {IS_WEB ? ( 294 <Button 295 label={_(msg`Close`)} 296 size="small" 297 shape="round" 298 + variant={IS_WEB ? 'ghost' : 'solid'} 299 color="secondary" 300 style={[ 301 a.absolute,
+3 -3
src/components/dialogs/Signin.tsx
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 - import {isNative} from '#/platform/detection' 7 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 8 import {useCloseAllActiveElements} from '#/state/util' 9 import {Logo} from '#/view/icons/Logo' ··· 13 import * as Dialog from '#/components/Dialog' 14 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 15 import {Text} from '#/components/Typography' 16 17 export function SigninDialog() { 18 const {signinDialogControl: control} = useGlobalDialogsControlContext() ··· 45 <Dialog.ScrollableInner 46 label={_(msg`Sign in to Bluesky or create a new account`)} 47 style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}> 48 - <View style={[!isNative && a.p_2xl]}> 49 <View 50 style={[ 51 a.flex_row, ··· 101 </Button> 102 </View> 103 104 - {isNative && <View style={{height: 10}} />} 105 </View> 106 107 <Dialog.Close />
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 7 import {useCloseAllActiveElements} from '#/state/util' 8 import {Logo} from '#/view/icons/Logo' ··· 12 import * as Dialog from '#/components/Dialog' 13 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 14 import {Text} from '#/components/Typography' 15 + import {IS_NATIVE} from '#/env' 16 17 export function SigninDialog() { 18 const {signinDialogControl: control} = useGlobalDialogsControlContext() ··· 45 <Dialog.ScrollableInner 46 label={_(msg`Sign in to Bluesky or create a new account`)} 47 style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}> 48 + <View style={[!IS_NATIVE && a.p_2xl]}> 49 <View 50 style={[ 51 a.flex_row, ··· 101 </Button> 102 </View> 103 104 + {IS_NATIVE && <View style={{height: 10}} />} 105 </View> 106 107 <Dialog.Close />
+4 -4
src/components/dialogs/StarterPackDialog.tsx
··· 12 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {logger} from '#/logger' 15 - import {isWeb} from '#/platform/detection' 16 import { 17 invalidateActorStarterPacksWithMembershipQuery, 18 useActorStarterPacksWithMembershipsQuery, ··· 32 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 33 import {Loader} from '#/components/Loader' 34 import {Text} from '#/components/Typography' 35 import * as bsky from '#/types/bsky' 36 37 type StarterPackWithMembership = ··· 91 const t = useTheme() 92 93 return ( 94 - <View style={[a.gap_2xl, {paddingTop: isWeb ? 100 : 64}]}> 95 <View style={[a.gap_xs, a.align_center]}> 96 <StarterPack 97 width={48} ··· 169 <View 170 style={[ 171 {justifyContent: 'space-between', flexDirection: 'row'}, 172 - isWeb ? a.mb_2xl : a.my_lg, 173 a.align_center, 174 ]}> 175 <Text style={[a.text_lg, a.font_semi_bold]}> ··· 232 onEndReachedThreshold={0.1} 233 ListHeaderComponent={listHeader} 234 ListEmptyComponent={<Empty onStartWizard={onStartWizard} />} 235 - style={isWeb ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]} 236 /> 237 ) 238 }
··· 12 import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {logger} from '#/logger' 15 import { 16 invalidateActorStarterPacksWithMembershipQuery, 17 useActorStarterPacksWithMembershipsQuery, ··· 31 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 32 import {Loader} from '#/components/Loader' 33 import {Text} from '#/components/Typography' 34 + import {IS_WEB} from '#/env' 35 import * as bsky from '#/types/bsky' 36 37 type StarterPackWithMembership = ··· 91 const t = useTheme() 92 93 return ( 94 + <View style={[a.gap_2xl, {paddingTop: IS_WEB ? 100 : 64}]}> 95 <View style={[a.gap_xs, a.align_center]}> 96 <StarterPack 97 width={48} ··· 169 <View 170 style={[ 171 {justifyContent: 'space-between', flexDirection: 'row'}, 172 + IS_WEB ? a.mb_2xl : a.my_lg, 173 a.align_center, 174 ]}> 175 <Text style={[a.text_lg, a.font_semi_bold]}> ··· 232 onEndReachedThreshold={0.1} 233 ListHeaderComponent={listHeader} 234 ListEmptyComponent={<Empty onStartWizard={onStartWizard} />} 235 + style={IS_WEB ? [a.px_md, {minHeight: 500}] : [a.px_2xl, a.pt_lg]} 236 /> 237 ) 238 }
+2 -2
src/components/dialogs/lists/CreateOrEditListDialog.tsx
··· 9 import {richTextToString} from '#/lib/strings/rich-text-helpers' 10 import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 11 import {logger} from '#/logger' 12 - import {isWeb} from '#/platform/detection' 13 import {type ImageMeta} from '#/state/gallery' 14 import { 15 useListCreateMutation, ··· 26 import {Loader} from '#/components/Loader' 27 import * as Prompt from '#/components/Prompt' 28 import {Text} from '#/components/Typography' 29 30 const DISPLAY_NAME_MAX_GRAPHEMES = 64 31 const DESCRIPTION_MAX_GRAPHEMES = 300 ··· 48 49 // 'You might lose unsaved changes' warning 50 useEffect(() => { 51 - if (isWeb && dirty) { 52 const abortController = new AbortController() 53 const {signal} = abortController 54 window.addEventListener('beforeunload', evt => evt.preventDefault(), {
··· 9 import {richTextToString} from '#/lib/strings/rich-text-helpers' 10 import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 11 import {logger} from '#/logger' 12 import {type ImageMeta} from '#/state/gallery' 13 import { 14 useListCreateMutation, ··· 25 import {Loader} from '#/components/Loader' 26 import * as Prompt from '#/components/Prompt' 27 import {Text} from '#/components/Typography' 28 + import {IS_WEB} from '#/env' 29 30 const DISPLAY_NAME_MAX_GRAPHEMES = 64 31 const DESCRIPTION_MAX_GRAPHEMES = 300 ··· 48 49 // 'You might lose unsaved changes' warning 50 useEffect(() => { 51 + if (IS_WEB && dirty) { 52 const abortController = new AbortController() 53 const {signal} = abortController 54 window.addEventListener('beforeunload', evt => evt.preventDefault(), {
+6 -6
src/components/dialogs/nuxs/ActivitySubscriptions.tsx
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 - import {isWeb} from '#/platform/detection' 8 import {atoms as a, useTheme, web} from '#/alf' 9 import {Button, ButtonText} from '#/components/Button' 10 import * as Dialog from '#/components/Dialog' 11 import {useNuxDialogContext} from '#/components/dialogs/nuxs' 12 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' 13 import {Text} from '#/components/Typography' 14 15 export function ActivitySubscriptionsNUX() { 16 const t = useTheme() ··· 44 a.overflow_hidden, 45 t.atoms.bg_contrast_25, 46 { 47 - gap: isWeb ? 16 : 24, 48 - paddingTop: isWeb ? 24 : 48, 49 borderTopLeftRadius: a.rounded_md.borderRadius, 50 borderTopRightRadius: a.rounded_md.borderRadius, 51 }, ··· 120 style={[ 121 a.align_center, 122 a.px_xl, 123 - isWeb ? [a.pt_xl, a.gap_xl, a.pb_sm] : [a.pt_3xl, a.gap_3xl], 124 ]}> 125 <View style={[a.gap_md, a.align_center]}> 126 <Text ··· 130 a.font_bold, 131 a.text_center, 132 { 133 - fontSize: isWeb ? 28 : 32, 134 maxWidth: 300, 135 }, 136 ]}> ··· 153 </Text> 154 </View> 155 156 - {!isWeb && ( 157 <Button 158 label={_(msg`Close`)} 159 size="large"
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {atoms as a, useTheme, web} from '#/alf' 8 import {Button, ButtonText} from '#/components/Button' 9 import * as Dialog from '#/components/Dialog' 10 import {useNuxDialogContext} from '#/components/dialogs/nuxs' 11 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' 12 import {Text} from '#/components/Typography' 13 + import {IS_WEB} from '#/env' 14 15 export function ActivitySubscriptionsNUX() { 16 const t = useTheme() ··· 44 a.overflow_hidden, 45 t.atoms.bg_contrast_25, 46 { 47 + gap: IS_WEB ? 16 : 24, 48 + paddingTop: IS_WEB ? 24 : 48, 49 borderTopLeftRadius: a.rounded_md.borderRadius, 50 borderTopRightRadius: a.rounded_md.borderRadius, 51 }, ··· 120 style={[ 121 a.align_center, 122 a.px_xl, 123 + IS_WEB ? [a.pt_xl, a.gap_xl, a.pb_sm] : [a.pt_3xl, a.gap_3xl], 124 ]}> 125 <View style={[a.gap_md, a.align_center]}> 126 <Text ··· 130 a.font_bold, 131 a.text_center, 132 { 133 + fontSize: IS_WEB ? 28 : 32, 134 maxWidth: 300, 135 }, 136 ]}> ··· 153 </Text> 154 </View> 155 156 + {!IS_WEB && ( 157 <Button 158 label={_(msg`Close`)} 159 size="large"
+5 -5
src/components/dialogs/nuxs/BookmarksAnnouncement.tsx
··· 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 - import {isWeb} from '#/platform/detection' 9 import {atoms as a, useTheme, web} from '#/alf' 10 import {transparentifyColor} from '#/alf/util/colorGeneration' 11 import {Button, ButtonText} from '#/components/Button' ··· 13 import {useNuxDialogContext} from '#/components/dialogs/nuxs' 14 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' 15 import {Text} from '#/components/Typography' 16 17 export function BookmarksAnnouncement() { 18 const t = useTheme() ··· 49 a.overflow_hidden, 50 { 51 gap: 16, 52 - paddingTop: isWeb ? 24 : 40, 53 borderTopLeftRadius: a.rounded_md.borderRadius, 54 borderTopRightRadius: a.rounded_md.borderRadius, 55 }, ··· 90 borderRadius: 24, 91 aspectRatio: 333 / 104, 92 }, 93 - isWeb 94 ? [ 95 { 96 boxShadow: `0px 10px 15px -3px ${transparentifyColor(t.palette.black, 0.2)}`, ··· 136 a.font_bold, 137 a.text_center, 138 { 139 - fontSize: isWeb ? 28 : 32, 140 maxWidth: 300, 141 }, 142 ]}> ··· 158 </Text> 159 </View> 160 161 - {!isWeb && ( 162 <Button 163 label={_(msg`Close`)} 164 size="large"
··· 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 import {atoms as a, useTheme, web} from '#/alf' 9 import {transparentifyColor} from '#/alf/util/colorGeneration' 10 import {Button, ButtonText} from '#/components/Button' ··· 12 import {useNuxDialogContext} from '#/components/dialogs/nuxs' 13 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' 14 import {Text} from '#/components/Typography' 15 + import {IS_WEB} from '#/env' 16 17 export function BookmarksAnnouncement() { 18 const t = useTheme() ··· 49 a.overflow_hidden, 50 { 51 gap: 16, 52 + paddingTop: IS_WEB ? 24 : 40, 53 borderTopLeftRadius: a.rounded_md.borderRadius, 54 borderTopRightRadius: a.rounded_md.borderRadius, 55 }, ··· 90 borderRadius: 24, 91 aspectRatio: 333 / 104, 92 }, 93 + IS_WEB 94 ? [ 95 { 96 boxShadow: `0px 10px 15px -3px ${transparentifyColor(t.palette.black, 0.2)}`, ··· 136 a.font_bold, 137 a.text_center, 138 { 139 + fontSize: IS_WEB ? 28 : 32, 140 maxWidth: 300, 141 }, 142 ]}> ··· 158 </Text> 159 </View> 160 161 + {!IS_WEB && ( 162 <Button 163 label={_(msg`Close`)} 164 size="large"
+3 -3
src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
··· 6 import {useLingui} from '@lingui/react' 7 8 import {logger} from '#/logger' 9 - import {isNative, isWeb} from '#/platform/detection' 10 import {atoms as a, useTheme, web} from '#/alf' 11 import {Button, ButtonText} from '#/components/Button' 12 import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist' ··· 17 isExistingUserAsOf, 18 } from '#/components/dialogs/nuxs/utils' 19 import {Text} from '#/components/Typography' 20 import {IS_E2E} from '#/env' 21 import {navigate} from '#/Navigation' 22 23 export const enabled = createIsEnabledCheck(props => { 24 return ( 25 !IS_E2E && 26 - isNative && 27 isExistingUserAsOf( 28 '2025-12-16T00:00:00.000Z', 29 props.currentProfile.createdAt, ··· 89 a.font_bold, 90 a.text_center, 91 { 92 - fontSize: isWeb ? 28 : 32, 93 maxWidth: 300, 94 }, 95 ]}>
··· 6 import {useLingui} from '@lingui/react' 7 8 import {logger} from '#/logger' 9 import {atoms as a, useTheme, web} from '#/alf' 10 import {Button, ButtonText} from '#/components/Button' 11 import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist' ··· 16 isExistingUserAsOf, 17 } from '#/components/dialogs/nuxs/utils' 18 import {Text} from '#/components/Typography' 19 + import {IS_NATIVE, IS_WEB} from '#/env' 20 import {IS_E2E} from '#/env' 21 import {navigate} from '#/Navigation' 22 23 export const enabled = createIsEnabledCheck(props => { 24 return ( 25 !IS_E2E && 26 + IS_NATIVE && 27 isExistingUserAsOf( 28 '2025-12-16T00:00:00.000Z', 29 props.currentProfile.createdAt, ··· 89 a.font_bold, 90 a.text_center, 91 { 92 + fontSize: IS_WEB ? 28 : 32, 93 maxWidth: 300, 94 }, 95 ]}>
+2 -2
src/components/dialogs/nuxs/InitialVerificationAnnouncement.tsx
··· 6 7 import {urls} from '#/lib/constants' 8 import {logger} from '#/logger' 9 - import {isNative} from '#/platform/detection' 10 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 import {Button, ButtonText} from '#/components/Button' 12 import * as Dialog from '#/components/Dialog' ··· 15 import {VerifierCheck} from '#/components/icons/VerifierCheck' 16 import {Link} from '#/components/Link' 17 import {Span, Text} from '#/components/Typography' 18 19 export 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"
··· 6 7 import {urls} from '#/lib/constants' 8 import {logger} from '#/logger' 9 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 10 import {Button, ButtonText} from '#/components/Button' 11 import * as Dialog from '#/components/Dialog' ··· 14 import {VerifierCheck} from '#/components/icons/VerifierCheck' 15 import {Link} from '#/components/Link' 16 import {Span, Text} from '#/components/Typography' 17 + import {IS_NATIVE} from '#/env' 18 19 export 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
··· 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 - import {isWeb} from '#/platform/detection' 9 import {atoms as a, select, useTheme, utils, web} from '#/alf' 10 import {Button, ButtonText} from '#/components/Button' 11 import * as Dialog from '#/components/Dialog' ··· 16 } from '#/components/dialogs/nuxs/utils' 17 import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' 18 import {Text} from '#/components/Typography' 19 import {IS_E2E} from '#/env' 20 21 export 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> 188 189 - {!isWeb && ( 190 <Button 191 label={_(msg`Close`)} 192 size="large"
··· 5 import {msg, Trans} from '@lingui/macro' 6 import {useLingui} from '@lingui/react' 7 8 import {atoms as a, select, useTheme, utils, web} from '#/alf' 9 import {Button, ButtonText} from '#/components/Button' 10 import * as Dialog from '#/components/Dialog' ··· 15 } from '#/components/dialogs/nuxs/utils' 16 import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' 17 import {Text} from '#/components/Typography' 18 + import {IS_WEB} from '#/env' 19 import {IS_E2E} from '#/env' 20 21 export 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> 188 189 + {!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={[
+4 -4
src/components/dms/ActionsWrapper.web.tsx
··· 89 : [a.ml_xs, {marginRight: 'auto'}], 90 ]}> 91 <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}> 92 - {({props, state, isNative, control}) => { 93 // always false, file is platform split 94 - if (isNative) return null 95 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 96 return ( 97 <Pressable ··· 111 }} 112 </EmojiReactionPicker> 113 <MessageContextMenu message={message}> 114 - {({props, state, isNative, control}) => { 115 // always false, file is platform split 116 - if (isNative) return null 117 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 118 return ( 119 <Pressable
··· 89 : [a.ml_xs, {marginRight: 'auto'}], 90 ]}> 91 <EmojiReactionPicker message={message} onEmojiSelect={onEmojiSelect}> 92 + {({props, state, IS_NATIVE, control}) => { 93 // always false, file is platform split 94 + if (IS_NATIVE) return null 95 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 96 return ( 97 <Pressable ··· 111 }} 112 </EmojiReactionPicker> 113 <MessageContextMenu message={message}> 114 + {({props, state, IS_NATIVE, control}) => { 115 // always false, file is platform split 116 + if (IS_NATIVE) return null 117 const showMenuTrigger = showActions || control.isOpen ? 1 : 0 118 return ( 119 <Pressable
+2 -2
src/components/dms/AfterReportDialog.tsx
··· 7 import type React from 'react' 8 9 import {type NavigationProp} from '#/lib/routes/types' 10 - import {isNative} from '#/platform/detection' 11 import {useProfileShadow} from '#/state/cache/profile-shadow' 12 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 13 import { ··· 21 import * as Toggle from '#/components/forms/Toggle' 22 import {Loader} from '#/components/Loader' 23 import {Text} from '#/components/Typography' 24 25 type ReportDialogParams = { 26 convoId: string ··· 130 onMutate: () => { 131 if (currentScreen === 'conversation') { 132 navigation.dispatch( 133 - StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}), 134 ) 135 } 136 },
··· 7 import type React from 'react' 8 9 import {type NavigationProp} from '#/lib/routes/types' 10 import {useProfileShadow} from '#/state/cache/profile-shadow' 11 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 12 import { ··· 20 import * as Toggle from '#/components/forms/Toggle' 21 import {Loader} from '#/components/Loader' 22 import {Text} from '#/components/Typography' 23 + import {IS_NATIVE} from '#/env' 24 25 type ReportDialogParams = { 26 convoId: string ··· 130 onMutate: () => { 131 if (currentScreen === 'conversation') { 132 navigation.dispatch( 133 + StackActions.replace('Messages', IS_NATIVE ? {animation: 'pop'} : {}), 134 ) 135 } 136 },
+3 -3
src/components/dms/ChatEmptyPill.tsx
··· 12 import {ScaleAndFadeIn} from '#/lib/custom-animations/ScaleAndFade' 13 import {ShrinkAndPop} from '#/lib/custom-animations/ShrinkAndPop' 14 import {useHaptics} from '#/lib/haptics' 15 - import {isWeb} from '#/platform/detection' 16 import {atoms as a, useTheme} from '#/alf' 17 import {Text} from '#/components/Typography' 18 19 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 20 ··· 41 }, [_]) 42 43 const onPressIn = React.useCallback(() => { 44 - if (isWeb) return 45 scale.set(() => withTiming(1.075, {duration: 100})) 46 }, [scale]) 47 48 const onPressOut = React.useCallback(() => { 49 - if (isWeb) return 50 scale.set(() => withTiming(1, {duration: 100})) 51 }, [scale]) 52
··· 12 import {ScaleAndFadeIn} from '#/lib/custom-animations/ScaleAndFade' 13 import {ShrinkAndPop} from '#/lib/custom-animations/ShrinkAndPop' 14 import {useHaptics} from '#/lib/haptics' 15 import {atoms as a, useTheme} from '#/alf' 16 import {Text} from '#/components/Typography' 17 + import {IS_WEB} from '#/env' 18 19 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 20 ··· 41 }, [_]) 42 43 const onPressIn = React.useCallback(() => { 44 + if (IS_WEB) return 45 scale.set(() => withTiming(1.075, {duration: 100})) 46 }, [scale]) 47 48 const onPressOut = React.useCallback(() => { 49 + if (IS_WEB) return 50 scale.set(() => withTiming(1, {duration: 100})) 51 }, [scale]) 52
+2 -2
src/components/dms/LeaveConvoPrompt.tsx
··· 3 import {StackActions, useNavigation} from '@react-navigation/native' 4 5 import {type NavigationProp} from '#/lib/routes/types' 6 - import {isNative} from '#/platform/detection' 7 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 8 import * as Toast from '#/view/com/util/Toast' 9 import {type DialogOuterProps} from '#/components/Dialog' 10 import * as Prompt from '#/components/Prompt' 11 12 export function LeaveConvoPrompt({ 13 control, ··· 27 onMutate: () => { 28 if (currentScreen === 'conversation') { 29 navigation.dispatch( 30 - StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}), 31 ) 32 } 33 },
··· 3 import {StackActions, useNavigation} from '@react-navigation/native' 4 5 import {type NavigationProp} from '#/lib/routes/types' 6 import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 7 import * as Toast from '#/view/com/util/Toast' 8 import {type DialogOuterProps} from '#/components/Dialog' 9 import * as Prompt from '#/components/Prompt' 10 + import {IS_NATIVE} from '#/env' 11 12 export function LeaveConvoPrompt({ 13 control, ··· 27 onMutate: () => { 28 if (currentScreen === 'conversation') { 29 navigation.dispatch( 30 + StackActions.replace('Messages', IS_NATIVE ? {animation: 'pop'} : {}), 31 ) 32 } 33 },
+2 -2
src/components/dms/MessageContextMenu.tsx
··· 8 import {useTranslate} from '#/lib/hooks/useTranslate' 9 import {richTextToString} from '#/lib/strings/rich-text-helpers' 10 import {logger} from '#/logger' 11 - import {isNative} from '#/platform/detection' 12 import {useConvoActive} from '#/state/messages/convo' 13 import {useLanguagePrefs} from '#/state/preferences' 14 import {useSession} from '#/state/session' ··· 23 import {ReportDialog} from '#/components/moderation/ReportDialog' 24 import * as Prompt from '#/components/Prompt' 25 import {usePromptControl} from '#/components/Prompt' 26 import {EmojiReactionPicker} from './EmojiReactionPicker' 27 import {hasReachedReactionLimit} from './util' 28 ··· 112 return ( 113 <> 114 <ContextMenu.Root> 115 - {isNative && ( 116 <ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}> 117 <EmojiReactionPicker 118 message={message}
··· 8 import {useTranslate} from '#/lib/hooks/useTranslate' 9 import {richTextToString} from '#/lib/strings/rich-text-helpers' 10 import {logger} from '#/logger' 11 import {useConvoActive} from '#/state/messages/convo' 12 import {useLanguagePrefs} from '#/state/preferences' 13 import {useSession} from '#/state/session' ··· 22 import {ReportDialog} from '#/components/moderation/ReportDialog' 23 import * as Prompt from '#/components/Prompt' 24 import {usePromptControl} from '#/components/Prompt' 25 + import {IS_NATIVE} from '#/env' 26 import {EmojiReactionPicker} from './EmojiReactionPicker' 27 import {hasReachedReactionLimit} from './util' 28 ··· 112 return ( 113 <> 114 <ContextMenu.Root> 115 + {IS_NATIVE && ( 116 <ContextMenu.AuxiliaryView align={isFromSelf ? 'right' : 'left'}> 117 <EmojiReactionPicker 118 message={message}
+3 -3
src/components/dms/MessageItem.tsx
··· 21 import {useLingui} from '@lingui/react' 22 23 import {sanitizeDisplayName} from '#/lib/strings/display-names' 24 - import {isNative} from '#/platform/detection' 25 import {useConvoActive} from '#/state/messages/convo' 26 import {type ConvoItem} from '#/state/messages/convo/types' 27 import {useSession} from '#/state/session' ··· 32 import {InlineLinkText} from '#/components/Link' 33 import {RichText} from '#/components/RichText' 34 import {Text} from '#/components/Typography' 35 import {DateDivider} from './DateDivider' 36 import {MessageItemEmbed} from './MessageItemEmbed' 37 import {localDateString} from './util' ··· 218 </View> 219 )} 220 221 - {isNative && appliedReactions} 222 </ActionsWrapper> 223 224 - {!isNative && appliedReactions} 225 226 {isLastInGroup && ( 227 <MessageItemMetadata
··· 21 import {useLingui} from '@lingui/react' 22 23 import {sanitizeDisplayName} from '#/lib/strings/display-names' 24 import {useConvoActive} from '#/state/messages/convo' 25 import {type ConvoItem} from '#/state/messages/convo/types' 26 import {useSession} from '#/state/session' ··· 31 import {InlineLinkText} from '#/components/Link' 32 import {RichText} from '#/components/RichText' 33 import {Text} from '#/components/Typography' 34 + import {IS_NATIVE} from '#/env' 35 import {DateDivider} from './DateDivider' 36 import {MessageItemEmbed} from './MessageItemEmbed' 37 import {localDateString} from './util' ··· 218 </View> 219 )} 220 221 + {IS_NATIVE && appliedReactions} 222 </ActionsWrapper> 223 224 + {!IS_NATIVE && appliedReactions} 225 226 {isLastInGroup && ( 227 <MessageItemMetadata
+2 -2
src/components/dms/MessagesListHeader.tsx
··· 10 11 import {makeProfileLink} from '#/lib/routes/links' 12 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 - import {isWeb} from '#/platform/detection' 14 import {type Shadow} from '#/state/cache/profile-shadow' 15 import {isConvoActive, useConvo} from '#/state/messages/convo' 16 import {type ConvoItem} from '#/state/messages/convo/types' ··· 24 import {Text} from '#/components/Typography' 25 import {useSimpleVerificationState} from '#/components/verification' 26 import {VerificationCheck} from '#/components/verification/VerificationCheck' 27 28 - const PFP_SIZE = isWeb ? 40 : Layout.HEADER_SLOT_SIZE 29 30 export function MessagesListHeader({ 31 profile,
··· 10 11 import {makeProfileLink} from '#/lib/routes/links' 12 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 import {type Shadow} from '#/state/cache/profile-shadow' 14 import {isConvoActive, useConvo} from '#/state/messages/convo' 15 import {type ConvoItem} from '#/state/messages/convo/types' ··· 23 import {Text} from '#/components/Typography' 24 import {useSimpleVerificationState} from '#/components/verification' 25 import {VerificationCheck} from '#/components/verification/VerificationCheck' 26 + import {IS_WEB} from '#/env' 27 28 + const PFP_SIZE = IS_WEB ? 40 : Layout.HEADER_SLOT_SIZE 29 30 export function MessagesListHeader({ 31 profile,
+5 -5
src/components/dms/NewMessagesPill.tsx
··· 14 ScaleAndFadeOut, 15 } from '#/lib/custom-animations/ScaleAndFade' 16 import {useHaptics} from '#/lib/haptics' 17 - import {isAndroid, isIOS, isWeb} from '#/platform/detection' 18 import {atoms as a, useTheme} from '#/alf' 19 import {Text} from '#/components/Typography' 20 21 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 22 ··· 28 const t = useTheme() 29 const playHaptic = useHaptics() 30 const {bottom: bottomInset} = useSafeAreaInsets() 31 - const bottomBarHeight = isIOS ? 42 : isAndroid ? 60 : 0 32 - const bottomOffset = isWeb ? 0 : bottomInset + bottomBarHeight 33 34 const scale = useSharedValue(1) 35 36 const onPressIn = React.useCallback(() => { 37 - if (isWeb) return 38 scale.set(() => withTiming(1.075, {duration: 100})) 39 }, [scale]) 40 41 const onPressOut = React.useCallback(() => { 42 - if (isWeb) return 43 scale.set(() => withTiming(1, {duration: 100})) 44 }, [scale]) 45
··· 14 ScaleAndFadeOut, 15 } from '#/lib/custom-animations/ScaleAndFade' 16 import {useHaptics} from '#/lib/haptics' 17 import {atoms as a, useTheme} from '#/alf' 18 import {Text} from '#/components/Typography' 19 + import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' 20 21 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 22 ··· 28 const t = useTheme() 29 const playHaptic = useHaptics() 30 const {bottom: bottomInset} = useSafeAreaInsets() 31 + const bottomBarHeight = IS_IOS ? 42 : IS_ANDROID ? 60 : 0 32 + const bottomOffset = IS_WEB ? 0 : bottomInset + bottomBarHeight 33 34 const scale = useSharedValue(1) 35 36 const onPressIn = React.useCallback(() => { 37 + if (IS_WEB) return 38 scale.set(() => withTiming(1.075, {duration: 100})) 39 }, [scale]) 40 41 const onPressOut = React.useCallback(() => { 42 + if (IS_WEB) return 43 scale.set(() => withTiming(1, {duration: 100})) 44 }, [scale]) 45
+2 -2
src/components/forms/SearchInput.tsx
··· 4 import {useLingui} from '@lingui/react' 5 6 import {HITSLOP_10} from '#/lib/constants' 7 - import {isNative} from '#/platform/detection' 8 import {atoms as a, useTheme} from '#/alf' 9 import {Button, ButtonIcon} from '#/components/Button' 10 import * as TextField from '#/components/forms/TextField' 11 import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass' 12 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 13 14 type 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}
··· 4 import {useLingui} from '@lingui/react' 5 6 import {HITSLOP_10} from '#/lib/constants' 7 import {atoms as a, useTheme} from '#/alf' 8 import {Button, ButtonIcon} from '#/components/Button' 9 import * as TextField from '#/components/forms/TextField' 10 import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass' 11 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 12 + import {IS_NATIVE} from '#/env' 13 14 type 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
··· 10 11 import {HITSLOP_10} from '#/lib/constants' 12 import {useHaptics} from '#/lib/haptics' 13 - import {isNative} from '#/platform/detection' 14 import { 15 atoms as a, 16 native, ··· 22 import {useInteractionState} from '#/components/hooks/useInteractionState' 23 import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check' 24 import {Text} from '#/components/Typography' 25 26 export * from './Panel' 27 ··· 562 ) 563 } 564 565 - export const Platform = isNative ? Switch : Checkbox
··· 10 11 import {HITSLOP_10} from '#/lib/constants' 12 import {useHaptics} from '#/lib/haptics' 13 import { 14 atoms as a, 15 native, ··· 21 import {useInteractionState} from '#/components/hooks/useInteractionState' 22 import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check' 23 import {Text} from '#/components/Typography' 24 + import {IS_NATIVE} from '#/env' 25 26 export * from './Panel' 27 ··· 562 ) 563 } 564 565 + export const Platform = IS_NATIVE ? Switch : Checkbox
+2 -2
src/components/hooks/useFullscreen.ts
··· 7 } from 'react' 8 9 import {isFirefox, isSafari} from '#/lib/browser' 10 - import {isWeb} from '#/platform/detection' 11 12 function fullscreenSubscribe(onChange: () => void) { 13 document.addEventListener('fullscreenchange', onChange) ··· 15 } 16 17 export 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' 8 9 import {isFirefox, isSafari} from '#/lib/browser' 10 + import {IS_WEB} from '#/env' 11 12 function fullscreenSubscribe(onChange: () => void) { 13 document.addEventListener('fullscreenchange', onChange) ··· 15 } 16 17 export 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 )
+2 -2
src/components/hooks/useStarterPackEntry.native.ts
··· 4 createStarterPackLinkFromAndroidReferrer, 5 httpStarterPackUriToAtUri, 6 } from '#/lib/strings/starter-pack' 7 - import {isAndroid} from '#/platform/detection' 8 import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 9 import {useSetActiveStarterPack} from '#/state/shell/starter-pack' 10 import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' 11 12 export function useStarterPackEntry() { ··· 32 ;(async () => { 33 let uri: string | null | undefined 34 35 - if (isAndroid) { 36 const res = await Referrer.getGooglePlayReferrerInfoAsync() 37 38 if (res && res.installReferrer) {
··· 4 createStarterPackLinkFromAndroidReferrer, 5 httpStarterPackUriToAtUri, 6 } from '#/lib/strings/starter-pack' 7 import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 8 import {useSetActiveStarterPack} from '#/state/shell/starter-pack' 9 + import {IS_ANDROID} from '#/env' 10 import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army' 11 12 export function useStarterPackEntry() { ··· 32 ;(async () => { 33 let uri: string | null | undefined 34 35 + if (IS_ANDROID) { 36 const res = await Referrer.getGooglePlayReferrerInfoAsync() 37 38 if (res && res.installReferrer) {
+2 -2
src/components/hooks/useWelcomeModal.ts
··· 1 import {useEffect, useState} from 'react' 2 3 - import {isWeb} from '#/platform/detection' 4 import {useSession} from '#/state/session' 5 6 export 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 =
··· 1 import {useEffect, useState} from 'react' 2 3 import {useSession} from '#/state/session' 4 + import {IS_WEB} from '#/env' 5 6 export 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
··· 11 import {useLingui} from '@lingui/react' 12 13 import {type Dimensions} from '#/lib/media/types' 14 - import {isNative} from '#/platform/detection' 15 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 16 import {atoms as a, useTheme} from '#/alf' 17 import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' 18 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 19 import {Text} from '#/components/Typography' 20 21 export 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}%`
··· 11 import {useLingui} from '@lingui/react' 12 13 import {type Dimensions} from '#/lib/media/types' 14 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 15 import {atoms as a, useTheme} from '#/alf' 16 import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' 17 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 18 import {Text} from '#/components/Typography' 19 + import {IS_NATIVE} from '#/env' 20 21 export 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}%`
+3 -3
src/components/intents/VerifyEmailIntentDialog.tsx
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 - import {isNative} from '#/platform/detection' 7 import {useAgent, useSession} from '#/state/session' 8 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 9 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 15 import {useIntentDialogs} from '#/components/intents/IntentDialogs' 16 import {Loader} from '#/components/Loader' 17 import {Text} from '#/components/Typography' 18 19 export 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>
··· 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {useAgent, useSession} from '#/state/session' 7 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 8 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 14 import {useIntentDialogs} from '#/components/intents/IntentDialogs' 15 import {Loader} from '#/components/Loader' 16 import {Text} from '#/components/Typography' 17 + import {IS_NATIVE} from '#/env' 18 19 export 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
··· 12 import {makeProfileLink} from '#/lib/routes/links' 13 import {sanitizeHandle} from '#/lib/strings/handles' 14 import {logger} from '#/logger' 15 - import {isAndroid} from '#/platform/detection' 16 import {useAgent, useSession} from '#/state/session' 17 import * as Toast from '#/view/com/util/Toast' 18 import {atoms as a, useBreakpoints, useTheme} from '#/alf' ··· 20 import * as Dialog from '#/components/Dialog' 21 import {InlineLinkText} from '#/components/Link' 22 import {Text} from '#/components/Typography' 23 import {Admonition} from '../Admonition' 24 import {Divider} from '../Divider' 25 import {Loader} from '../Loader' ··· 344 {isPending && <ButtonIcon icon={Loader} />} 345 </Button> 346 </View> 347 - {isAndroid && <View style={{height: 300}} />} 348 </> 349 ) 350 }
··· 12 import {makeProfileLink} from '#/lib/routes/links' 13 import {sanitizeHandle} from '#/lib/strings/handles' 14 import {logger} from '#/logger' 15 import {useAgent, useSession} from '#/state/session' 16 import * as Toast from '#/view/com/util/Toast' 17 import {atoms as a, useBreakpoints, useTheme} from '#/alf' ··· 19 import * as Dialog from '#/components/Dialog' 20 import {InlineLinkText} from '#/components/Link' 21 import {Text} from '#/components/Typography' 22 + import {IS_ANDROID} from '#/env' 23 import {Admonition} from '../Admonition' 24 import {Divider} from '../Divider' 25 import {Loader} from '../Loader' ··· 344 {isPending && <ButtonIcon icon={Loader} />} 345 </Button> 346 </View> 347 + {IS_ANDROID && <View style={{height: 300}} />} 348 </> 349 ) 350 }
+3 -3
src/components/moderation/ModerationDetailsDialog.tsx
··· 7 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 8 import {makeProfileLink} from '#/lib/routes/links' 9 import {listUriToHref} from '#/lib/strings/url-helpers' 10 - import {isNative} from '#/platform/detection' 11 import {useSession} from '#/state/session' 12 import {atoms as a, useGutters, useTheme} from '#/alf' 13 import * as Dialog from '#/components/Dialog' 14 import {InlineLinkText} from '#/components/Link' 15 import {type AppModerationCause} from '#/components/Pills' 16 import {Text} from '#/components/Typography' 17 18 export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog' 19 ··· 158 xGutters, 159 a.py_md, 160 a.border_t, 161 - !isNative && t.atoms.bg_contrast_25, 162 t.atoms.border_contrast_low, 163 { 164 borderBottomLeftRadius: a.rounded_md.borderRadius, ··· 219 </View> 220 )} 221 222 - {isNative && <View style={{height: 40}} />} 223 224 <Dialog.Close /> 225 </Dialog.ScrollableInner>
··· 7 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' 8 import {makeProfileLink} from '#/lib/routes/links' 9 import {listUriToHref} from '#/lib/strings/url-helpers' 10 import {useSession} from '#/state/session' 11 import {atoms as a, useGutters, useTheme} from '#/alf' 12 import * as Dialog from '#/components/Dialog' 13 import {InlineLinkText} from '#/components/Link' 14 import {type AppModerationCause} from '#/components/Pills' 15 import {Text} from '#/components/Typography' 16 + import {IS_NATIVE} from '#/env' 17 18 export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog' 19 ··· 158 xGutters, 159 a.py_md, 160 a.border_t, 161 + !IS_NATIVE && t.atoms.bg_contrast_25, 162 t.atoms.border_contrast_low, 163 { 164 borderBottomLeftRadius: a.rounded_md.borderRadius, ··· 219 </View> 220 )} 221 222 + {IS_NATIVE && <View style={{height: 40}} />} 223 224 <Dialog.Close /> 225 </Dialog.ScrollableInner>
+2 -2
src/components/moderation/ReportDialog/index.tsx
··· 8 import {getLabelingServiceTitle} from '#/lib/moderation' 9 import {sanitizeHandle} from '#/lib/strings/handles' 10 import {Logger} from '#/logger' 11 - import {isNative} from '#/platform/detection' 12 import {useMyLabelersQuery} from '#/state/queries/preferences' 13 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 14 import {UserAvatar} from '#/view/com/util/UserAvatar' ··· 29 import {createStaticClick, InlineLinkText, Link} from '#/components/Link' 30 import {Loader} from '#/components/Loader' 31 import {Text} from '#/components/Typography' 32 import {useSubmitReportMutation} from './action' 33 import { 34 BSKY_LABELER_ONLY_REPORT_REASONS, ··· 253 label={_(msg`Report dialog`)} 254 ref={ref} 255 style={[a.w_full, {maxWidth: 500}]}> 256 - <View style={[a.gap_2xl, isNative && a.pt_md]}> 257 <StepOuter> 258 <StepTitle 259 index={1}
··· 8 import {getLabelingServiceTitle} from '#/lib/moderation' 9 import {sanitizeHandle} from '#/lib/strings/handles' 10 import {Logger} from '#/logger' 11 import {useMyLabelersQuery} from '#/state/queries/preferences' 12 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 13 import {UserAvatar} from '#/view/com/util/UserAvatar' ··· 28 import {createStaticClick, InlineLinkText, Link} from '#/components/Link' 29 import {Loader} from '#/components/Loader' 30 import {Text} from '#/components/Typography' 31 + import {IS_NATIVE} from '#/env' 32 import {useSubmitReportMutation} from './action' 33 import { 34 BSKY_LABELER_ONLY_REPORT_REASONS, ··· 253 label={_(msg`Report dialog`)} 254 ref={ref} 255 style={[a.w_full, {maxWidth: 500}]}> 256 + <View style={[a.gap_2xl, IS_NATIVE && a.pt_md]}> 257 <StepOuter> 258 <StepTitle 259 index={1}
+11
src/env/index.ts
··· 1 import {nativeBuildVersion} from 'expo-application' 2 3 import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common' ··· 17 export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${ 18 __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' 19 })`
··· 1 + import {Platform} from 'react-native' 2 import {nativeBuildVersion} from 'expo-application' 3 4 import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common' ··· 18 export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${ 19 __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod' 20 })` 21 + 22 + /** 23 + * Platform detection 24 + */ 25 + export const IS_IOS: boolean = Platform.OS === 'ios' 26 + export const IS_ANDROID: boolean = Platform.OS === 'android' 27 + export const IS_NATIVE: boolean = true 28 + export const IS_WEB: boolean = false 29 + export const IS_WEB_MOBILE: boolean = false 30 + export const IS_WEB_MOBILE_IOS: boolean = false
+13
src/env/index.web.ts
··· 13 * The short commit hash and environment of the current bundle. 14 */ 15 export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
··· 13 * The short commit hash and environment of the current bundle. 14 */ 15 export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})` 16 + 17 + /** 18 + * Platform detection 19 + */ 20 + export const IS_IOS: boolean = false 21 + export const IS_ANDROID: boolean = false 22 + export const IS_NATIVE: boolean = false 23 + export const IS_WEB: boolean = true 24 + // @ts-ignore we know window exists -prf 25 + export const IS_WEB_MOBILE: boolean = global.window.matchMedia( 26 + 'only screen and (max-width: 1300px)', 27 + )?.matches 28 + export const IS_WEB_MOBILE_IOS: boolean = /iPhone/.test(navigator.userAgent)
+2 -2
src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx
··· 3 import {useLingui} from '@lingui/react' 4 5 import {useCleanError} from '#/lib/hooks/useCleanError' 6 - import {isNative} from '#/platform/detection' 7 import {atoms as a, web} from '#/alf' 8 import {Admonition} from '#/components/Admonition' 9 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 11 import {Loader} from '#/components/Loader' 12 import * as Toast from '#/components/Toast' 13 import {Span, Text} from '#/components/Typography' 14 import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' 15 import { 16 type LiveEventFeed, ··· 146 </ButtonText> 147 {isHidingAllFeeds && <ButtonIcon icon={Loader} />} 148 </Button> 149 - {isNative && ( 150 <Button 151 label={_(msg`Cancel`)} 152 size="large"
··· 3 import {useLingui} from '@lingui/react' 4 5 import {useCleanError} from '#/lib/hooks/useCleanError' 6 import {atoms as a, web} from '#/alf' 7 import {Admonition} from '#/components/Admonition' 8 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 10 import {Loader} from '#/components/Loader' 11 import * as Toast from '#/components/Toast' 12 import {Span, Text} from '#/components/Typography' 13 + import {IS_NATIVE} from '#/env' 14 import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences' 15 import { 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
··· 3 import {useMutation, useQueryClient} from '@tanstack/react-query' 4 5 import {logger} from '#/logger' 6 - import {isWeb} from '#/platform/detection' 7 import { 8 preferencesQueryKey, 9 usePreferencesQuery, 10 } from '#/state/queries/preferences' 11 import {useAgent} from '#/state/session' 12 import * as env from '#/env' 13 import { 14 type LiveEventFeed, ··· 41 const agent = useAgent() 42 43 useEffect(() => { 44 - if (env.IS_DEV && isWeb && typeof window !== 'undefined') { 45 // @ts-ignore 46 window.__updateLiveEventPreferences = async ( 47 action: LiveEventPreferencesAction,
··· 3 import {useMutation, useQueryClient} from '@tanstack/react-query' 4 5 import {logger} from '#/logger' 6 import { 7 preferencesQueryKey, 8 usePreferencesQuery, 9 } from '#/state/queries/preferences' 10 import {useAgent} from '#/state/session' 11 + import {IS_WEB} from '#/env' 12 import * as env from '#/env' 13 import { 14 type LiveEventFeed, ··· 41 const agent = useAgent() 42 43 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
··· 3 import * as Location from 'expo-location' 4 import {createPermissionHook} from 'expo-modules-core' 5 6 - import {isNative} from '#/platform/detection' 7 import * as debug from '#/geolocation/debug' 8 import {logger} from '#/geolocation/logger' 9 import {type Geolocation} from '#/geolocation/types' ··· 118 const synced = useRef(false) 119 const [status] = useForegroundPermissions() 120 useEffect(() => { 121 - if (!isNative) return 122 123 async function get() { 124 // no need to set this more than once per session
··· 3 import * as Location from 'expo-location' 4 import {createPermissionHook} from 'expo-modules-core' 5 6 + import {IS_NATIVE} from '#/env' 7 import * as debug from '#/geolocation/debug' 8 import {logger} from '#/geolocation/logger' 9 import {type Geolocation} from '#/geolocation/types' ··· 118 const synced = useRef(false) 119 const [status] = useForegroundPermissions() 120 useEffect(() => { 121 + if (!IS_NATIVE) return 122 123 async function get() { 124 // no need to set this more than once per session
+2 -2
src/geolocation/util.ts
··· 1 import {type LocationGeocodedAddress} from 'expo-location' 2 3 - import {isAndroid} from '#/platform/detection' 4 import {logger} from '#/geolocation/logger' 5 import {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
··· 1 import {type LocationGeocodedAddress} from 'expo-location' 2 3 + import {IS_ANDROID} from '#/env' 4 import {logger} from '#/geolocation/logger' 5 import {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
··· 1 import {AtUri} from '@atproto/api' 2 3 import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' 4 - import {isWeb} from '#/platform/detection' 5 import {type UsePreferencesQueryResponse} from '#/state/queries/preferences' 6 7 let debugTopics = '' 8 - if (isWeb && typeof window !== 'undefined') { 9 const params = new URLSearchParams(window.location.search) 10 debugTopics = params.get('debug_topics') ?? '' 11 }
··· 1 import {AtUri} from '@atproto/api' 2 3 import {BSKY_FEED_OWNER_DIDS} from '#/lib/constants' 4 import {type UsePreferencesQueryResponse} from '#/state/queries/preferences' 5 + import {IS_WEB} from '#/env' 6 7 let debugTopics = '' 8 + if (IS_WEB && typeof window !== 'undefined') { 9 const params = new URLSearchParams(window.location.search) 10 debugTopics = params.get('debug_topics') ?? '' 11 }
+1 -1
src/lib/browser.native.ts
··· 1 export const isSafari = false 2 export const isFirefox = false 3 export const isTouchDevice = true 4 - export const isAndroidWeb = false 5 export const isHighDPI = true
··· 1 export const isSafari = false 2 export const isFirefox = false 3 export const isTouchDevice = true 4 + export const IS_ANDROIDWeb = false 5 export const isHighDPI = true
+1 -1
src/lib/browser.ts
··· 4 ) 5 export const isFirefox = /firefox|fxios/i.test(navigator.userAgent) 6 export const isTouchDevice = window.matchMedia('(pointer: coarse)').matches 7 - export const isAndroidWeb = 8 /android/i.test(navigator.userAgent) && isTouchDevice 9 export const isHighDPI = window.matchMedia('(min-resolution: 2dppx)').matches
··· 4 ) 5 export const isFirefox = /firefox|fxios/i.test(navigator.userAgent) 6 export const isTouchDevice = window.matchMedia('(pointer: coarse)').matches 7 + export const IS_ANDROIDWeb = 8 /android/i.test(navigator.userAgent) && isTouchDevice 9 export const isHighDPI = window.matchMedia('(min-resolution: 2dppx)').matches
+3 -3
src/lib/custom-animations/AccordionAnimation.tsx
··· 13 withTiming, 14 } from 'react-native-reanimated' 15 16 - import {isIOS, isWeb} from '#/platform/detection' 17 18 type AccordionAnimationProps = React.PropsWithChildren<{ 19 isExpanded: boolean ··· 66 style={style} 67 entering={FadeInUp.duration(duration)} 68 exiting={FadeOutUp.duration(duration / 2)} 69 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 70 {children} 71 </Animated.View> 72 ) 73 } 74 75 export function AccordionAnimation(props: AccordionAnimationProps) { 76 - return isWeb ? <WebAccordion {...props} /> : <MobileAccordion {...props} /> 77 }
··· 13 withTiming, 14 } from 'react-native-reanimated' 15 16 + import {IS_IOS, IS_WEB} from '#/env' 17 18 type AccordionAnimationProps = React.PropsWithChildren<{ 19 isExpanded: boolean ··· 66 style={style} 67 entering={FadeInUp.duration(duration)} 68 exiting={FadeOutUp.duration(duration / 2)} 69 + pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 70 {children} 71 </Animated.View> 72 ) 73 } 74 75 export function AccordionAnimation(props: AccordionAnimationProps) { 76 + return IS_WEB ? <WebAccordion {...props} /> : <MobileAccordion {...props} /> 77 }
+2 -2
src/lib/custom-animations/PressableScale.tsx
··· 13 } from 'react-native-reanimated' 14 15 import {isTouchDevice} from '#/lib/browser' 16 - import {isNative} from '#/platform/detection' 17 18 - const DEFAULT_TARGET_SCALE = isNative || isTouchDevice ? 0.98 : 1 19 20 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 21
··· 13 } from 'react-native-reanimated' 14 15 import {isTouchDevice} from '#/lib/browser' 16 + import {IS_NATIVE} from '#/env' 17 18 + const DEFAULT_TARGET_SCALE = IS_NATIVE || isTouchDevice ? 0.98 : 1 19 20 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) 21
+3 -3
src/lib/haptics.ts
··· 2 import * as Device from 'expo-device' 3 import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' 4 5 - import {isIOS, isWeb} from '#/platform/detection' 6 import {useHapticsDisabled} from '#/state/preferences/disable-haptics' 7 8 export function useHaptics() { 9 const isHapticsDisabled = useHapticsDisabled() 10 11 return React.useCallback( 12 (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => { 13 - if (isHapticsDisabled || isWeb) { 14 return 15 } 16 17 // 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)
··· 2 import * as Device from 'expo-device' 3 import {impactAsync, ImpactFeedbackStyle} from 'expo-haptics' 4 5 import {useHapticsDisabled} from '#/state/preferences/disable-haptics' 6 + import {IS_IOS, IS_WEB} from '#/env' 7 8 export function useHaptics() { 9 const isHapticsDisabled = useHapticsDisabled() 10 11 return React.useCallback( 12 (strength: 'Light' | 'Medium' | 'Heavy' = 'Medium') => { 13 + if (isHapticsDisabled || IS_WEB) { 14 return 15 } 16 17 // 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
··· 3 import {useLingui} from '@lingui/react' 4 5 import {logger} from '#/logger' 6 - import {isWeb} from '#/platform/detection' 7 import {type SessionAccount, useSessionApi} from '#/state/session' 8 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 9 import * as Toast from '#/view/com/util/Toast' 10 import {logEvent} from '../statsig/statsig' 11 import {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
··· 3 import {useLingui} from '@lingui/react' 4 5 import {logger} from '#/logger' 6 import {type SessionAccount, useSessionApi} from '#/state/session' 7 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 8 import * as Toast from '#/view/com/util/Toast' 9 + import {IS_WEB} from '#/env' 10 import {logEvent} from '../statsig/statsig' 11 import {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
+2 -2
src/lib/hooks/useBottomBarOffset.ts
··· 2 3 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 4 import {clamp} from '#/lib/numbers' 5 - import {isWeb} from '#/platform/detection' 6 7 export function useBottomBarOffset(modifier: number = 0) { 8 const {isTabletOrDesktop} = useWebMediaQueries() 9 const {bottom: bottomInset} = useSafeAreaInsets() 10 return ( 11 - (isWeb && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) + 12 modifier 13 ) 14 }
··· 2 3 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 4 import {clamp} from '#/lib/numbers' 5 + import {IS_WEB} from '#/env' 6 7 export function useBottomBarOffset(modifier: number = 0) { 8 const {isTabletOrDesktop} = useWebMediaQueries() 9 const {bottom: bottomInset} = useSafeAreaInsets() 10 return ( 11 + (IS_WEB && isTabletOrDesktop ? 0 : clamp(60 + bottomInset, 60, 75)) + 12 modifier 13 ) 14 }
+3 -3
src/lib/hooks/useIntentHandler.ts
··· 6 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 7 import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 8 import {logger} from '#/logger' 9 - import {isIOS, isNative} from '#/platform/detection' 10 import {useSession} from '#/state/session' 11 import {useCloseAllActiveElements} from '#/state/util' 12 import {useIntentDialogs} from '#/components/intents/IntentDialogs' 13 import {Referrer} from '../../../modules/expo-bluesky-swiss-army' 14 import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' 15 ··· 29 30 React.useEffect(() => { 31 const handleIncomingURL = async (url: string) => { 32 - if (isIOS) { 33 // Close in-app browser if it's open (iOS only) 34 await WebBrowser.dismissBrowser().catch(() => {}) 35 } ··· 150 setTimeout(() => { 151 openComposer({ 152 text: text ?? undefined, 153 - imageUris: isNative ? imageUris : undefined, 154 }) 155 }, 500) 156 },
··· 6 import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 7 import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 8 import {logger} from '#/logger' 9 import {useSession} from '#/state/session' 10 import {useCloseAllActiveElements} from '#/state/util' 11 import {useIntentDialogs} from '#/components/intents/IntentDialogs' 12 + import {IS_IOS, IS_NATIVE} from '#/env' 13 import {Referrer} from '../../../modules/expo-bluesky-swiss-army' 14 import {useApplyPullRequestOTAUpdate} from './useOTAUpdates' 15 ··· 29 30 React.useEffect(() => { 31 const handleIncomingURL = async (url: string) => { 32 + if (IS_IOS) { 33 // Close in-app browser if it's open (iOS only) 34 await WebBrowser.dismissBrowser().catch(() => {}) 35 } ··· 150 setTimeout(() => { 151 openComposer({ 152 text: text ?? undefined, 153 + imageUris: IS_NATIVE ? imageUris : undefined, 154 }) 155 }, 500) 156 },
+3 -3
src/lib/hooks/useIsKeyboardVisible.ts
··· 1 import {useEffect, useState} from 'react' 2 import {Keyboard} from 'react-native' 3 4 - import {isIOS} from '#/platform/detection' 5 6 export function useIsKeyboardVisible({ 7 iosUseWillEvents, ··· 14 // only iOS supports the "will" events 15 // -prf 16 const showEvent = 17 - isIOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow' 18 const hideEvent = 19 - isIOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide' 20 21 useEffect(() => { 22 const keyboardShowListener = Keyboard.addListener(showEvent, () =>
··· 1 import {useEffect, useState} from 'react' 2 import {Keyboard} from 'react-native' 3 4 + import {IS_IOS} from '#/env' 5 6 export function useIsKeyboardVisible({ 7 iosUseWillEvents, ··· 14 // only iOS supports the "will" events 15 // -prf 16 const showEvent = 17 + IS_IOS && iosUseWillEvents ? 'keyboardWillShow' : 'keyboardDidShow' 18 const hideEvent = 19 + IS_IOS && iosUseWillEvents ? 'keyboardWillHide' : 'keyboardDidHide' 20 21 useEffect(() => { 22 const keyboardShowListener = Keyboard.addListener(showEvent, () =>
+3 -3
src/lib/hooks/useNotificationHandler.ts
··· 9 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 10 import {logger as notyLogger} from '#/lib/notifications/util' 11 import {type NavigationProp} from '#/lib/routes/types' 12 - import {isAndroid, isIOS} from '#/platform/detection' 13 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 14 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 15 import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' ··· 17 import {useSession} from '#/state/session' 18 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 19 import {useCloseAllActiveElements} from '#/state/util' 20 import {resetToTab} from '#/Navigation' 21 import {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 } 380 381 const payload = ( 382 - isIOS ? e.request.trigger.payload : e.request.content.data 383 ) as NotificationPayload 384 385 if (payload && payload.reason) {
··· 9 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 10 import {logger as notyLogger} from '#/lib/notifications/util' 11 import {type NavigationProp} from '#/lib/routes/types' 12 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 13 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 14 import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' ··· 16 import {useSession} from '#/state/session' 17 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 18 import {useCloseAllActiveElements} from '#/state/util' 19 + import {IS_ANDROID, IS_IOS} from '#/env' 20 import {resetToTab} from '#/Navigation' 21 import {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 } 380 381 const payload = ( 382 + IS_IOS ? e.request.trigger.payload : e.request.content.data 383 ) as NotificationPayload 384 385 if (payload && payload.reason) {
+4 -4
src/lib/hooks/useOTAUpdates.ts
··· 12 13 import {isNetworkError} from '#/lib/strings/errors' 14 import {logger} from '#/logger' 15 - import {isAndroid, isIOS} from '#/platform/detection' 16 import {IS_TESTFLIGHT} from '#/env' 17 18 const MINIMUM_MINIMIZE_TIME = 15 * 60e3 19 20 async 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}`, ··· 32 33 async 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 202 203 const subscription = AppState.addEventListener( 204 'change',
··· 12 13 import {isNetworkError} from '#/lib/strings/errors' 14 import {logger} from '#/logger' 15 + import {IS_ANDROID, IS_IOS} from '#/env' 16 import {IS_TESTFLIGHT} from '#/env' 17 18 const MINIMUM_MINIMIZE_TIME = 15 * 60e3 19 20 async 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}`, ··· 32 33 async 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 202 203 const subscription = AppState.addEventListener( 204 'change',
+2 -2
src/lib/hooks/useOpenLink.ts
··· 12 toNiceDomain, 13 } from '#/lib/strings/url-helpers' 14 import {logger} from '#/logger' 15 - import {isNative} from '#/platform/detection' 16 import {useInAppBrowser} from '#/state/preferences/in-app-browser' 17 import {useTheme} from '#/alf' 18 import {useDialogContext} from '#/components/Dialog' 19 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 20 21 export function useOpenLink() { 22 const enabled = useInAppBrowser() ··· 41 } 42 } 43 44 - 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' 14 import {logger} from '#/logger' 15 import {useInAppBrowser} from '#/state/preferences/in-app-browser' 16 import {useTheme} from '#/alf' 17 import {useDialogContext} from '#/components/Dialog' 18 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 19 + import {IS_NATIVE} from '#/env' 20 21 export function useOpenLink() { 22 const enabled = useInAppBrowser() ··· 41 } 42 } 43 44 + 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
··· 2 import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera' 3 import * as MediaLibrary from 'expo-media-library' 4 5 - import {isWeb} from '#/platform/detection' 6 import {Alert} from '#/view/com/util/Alert' 7 8 const 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
··· 2 import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera' 3 import * as MediaLibrary from 'expo-media-library' 4 5 import {Alert} from '#/view/com/util/Alert' 6 + import {IS_WEB} from '#/env' 7 8 const 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
··· 2 import * as IntentLauncher from 'expo-intent-launcher' 3 4 import {getTranslatorLink} from '#/locale/helpers' 5 - import {isAndroid} from '#/platform/detection' 6 import {useOpenLink} from './useOpenLink' 7 8 export 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 (
··· 2 import * as IntentLauncher from 'expo-intent-launcher' 3 4 import {getTranslatorLink} from '#/locale/helpers' 5 + import {IS_ANDROID} from '#/env' 6 import {useOpenLink} from './useOpenLink' 7 8 export 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 (
+2 -2
src/lib/hooks/useWebMediaQueries.tsx
··· 1 import {useMediaQuery} from 'react-responsive' 2 3 - import {isNative} from '#/platform/detection' 4 5 /** 6 * @deprecated use `useBreakpoints` from `#/alf` instead ··· 11 const isMobile = useMediaQuery({maxWidth: 800 - 1}) 12 const isTabletOrMobile = isMobile || isTablet 13 const isTabletOrDesktop = isDesktop || isTablet 14 - if (isNative) { 15 return { 16 isMobile: true, 17 isTablet: false,
··· 1 import {useMediaQuery} from 'react-responsive' 2 3 + import {IS_NATIVE} from '#/env' 4 5 /** 6 * @deprecated use `useBreakpoints` from `#/alf` instead ··· 11 const isMobile = useMediaQuery({maxWidth: 800 - 1}) 12 const isTabletOrMobile = isMobile || isTablet 13 const isTabletOrDesktop = isDesktop || isTablet 14 + if (IS_NATIVE) { 15 return { 16 isMobile: true, 17 isTablet: false,
+4 -4
src/lib/media/manip.ts
··· 18 19 import {POST_IMG_MAX} from '#/lib/constants' 20 import {logger} from '#/logger' 21 - import {isAndroid, isIOS} from '#/platform/detection' 22 import {type PickerImage} from './picker.shared' 23 import {type Dimensions} from './types' 24 ··· 108 109 // 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 } 306 307 function 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 })
··· 18 19 import {POST_IMG_MAX} from '#/lib/constants' 20 import {logger} from '#/logger' 21 + import {IS_ANDROID, IS_IOS} from '#/env' 22 import {type PickerImage} from './picker.shared' 23 import {type Dimensions} from './types' 24 ··· 108 109 // 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 } 306 307 function 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' 6 import {t} from '@lingui/macro' 7 8 - import {isIOS, isWeb} from '#/platform/detection' 9 import {type ImageMeta} from '#/state/gallery' 10 import * as Toast from '#/view/com/util/Toast' 11 import {VIDEO_MAX_DURATION_MS} from '../constants' 12 import {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' 6 import {t} from '@lingui/macro' 7 8 import {type ImageMeta} from '#/state/gallery' 9 import * as Toast from '#/view/com/util/Toast' 10 + import {IS_IOS, IS_WEB} from '#/env' 11 import {VIDEO_MAX_DURATION_MS} from '../constants' 12 import {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
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 - import {isNative} from '#/platform/detection' 6 import * as Toast from '#/components/Toast' 7 import {saveImageToMediaLibrary} from './manip' 8 9 /** ··· 16 const {_} = useLingui() 17 return useCallback( 18 async (uri: string) => { 19 - if (!isNative) { 20 throw new Error('useSaveImageToMediaLibrary is native only') 21 } 22
··· 2 import {msg} from '@lingui/macro' 3 import {useLingui} from '@lingui/react' 4 5 import * as Toast from '#/components/Toast' 6 + import {IS_NATIVE} from '#/env' 7 import {saveImageToMediaLibrary} from './manip' 8 9 /** ··· 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
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 - import {isNative} from '#/platform/detection' 7 import * as Toast from '#/components/Toast' 8 import {saveImageToMediaLibrary} from './manip' 9 10 /** ··· 18 }) 19 return useCallback( 20 async (uri: string) => { 21 - if (!isNative) { 22 throw new Error('useSaveImageToMediaLibrary is native only') 23 } 24
··· 3 import {msg} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import * as Toast from '#/components/Toast' 7 + import {IS_NATIVE} from '#/env' 8 import {saveImageToMediaLibrary} from './manip' 9 10 /** ··· 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' 14 import {logger as notyLogger} from '#/lib/notifications/util' 15 import {isNetworkError} from '#/lib/strings/errors' 16 - import {isNative} from '#/platform/detection' 17 import {type SessionAccount, useAgent, useSession} from '#/state/session' 18 import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 19 import {useAgeAssurance} from '#/ageAssurance' 20 import {IS_DEV} from '#/env' 21 22 /** ··· 140 }: { 141 isAgeRestricted?: boolean 142 } = {}) => { 143 - if (!isNative || IS_DEV) return 144 145 /** 146 * This will also fire the listener added via `addPushTokenListener`. That ··· 236 const permissions = await Notifications.getPermissionsAsync() 237 238 if ( 239 - !isNative || 240 permissions?.status === 'granted' || 241 (permissions?.status === 'denied' && !permissions.canAskAgain) 242 ) { ··· 277 } 278 279 export async function decrementBadgeCount(by: number) { 280 - if (!isNative) return 281 282 let count = await getBadgeCountAsync() 283 count -= by ··· 295 } 296 297 export async function unregisterPushToken(agents: AtpAgent[]) { 298 - if (!isNative) return 299 300 try { 301 const token = await getPushToken()
··· 13 } from '#/lib/constants' 14 import {logger as notyLogger} from '#/lib/notifications/util' 15 import {isNetworkError} from '#/lib/strings/errors' 16 import {type SessionAccount, useAgent, useSession} from '#/state/session' 17 import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 18 import {useAgeAssurance} from '#/ageAssurance' 19 + import {IS_NATIVE} from '#/env' 20 import {IS_DEV} from '#/env' 21 22 /** ··· 140 }: { 141 isAgeRestricted?: boolean 142 } = {}) => { 143 + if (!IS_NATIVE || IS_DEV) return 144 145 /** 146 * This will also fire the listener added via `addPushTokenListener`. That ··· 236 const permissions = await Notifications.getPermissionsAsync() 237 238 if ( 239 + !IS_NATIVE || 240 permissions?.status === 'granted' || 241 (permissions?.status === 'denied' && !permissions.canAskAgain) 242 ) { ··· 277 } 278 279 export async function decrementBadgeCount(by: number) { 280 + if (!IS_NATIVE) return 281 282 let count = await getBadgeCountAsync() 283 count -= by ··· 295 } 296 297 export async function unregisterPushToken(agents: AtpAgent[]) { 298 + if (!IS_NATIVE) return 299 300 try { 301 const token = await getPushToken()
+3 -3
src/lib/react-query.tsx
··· 9 } from '@tanstack/react-query-persist-client' 10 import type React from 'react' 11 12 - import {isNative, isWeb} from '#/platform/detection' 13 import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' 14 15 declare global { 16 interface Window { ··· 87 }, 2000) 88 89 focusManager.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' 10 import type React from 'react' 11 12 import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' 13 + import {IS_NATIVE, IS_WEB} from '#/env' 14 15 declare global { 16 interface Window { ··· 87 }, 2000) 88 89 focusManager.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 5 import {t} from '@lingui/macro' 6 7 - import {isAndroid, isIOS} from '#/platform/detection' 8 import * as Toast from '#/view/com/util/Toast' 9 10 /** 11 * This function shares a URL using the native Share API if available, or copies it to the clipboard ··· 14 * clipboard. 15 */ 16 export 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 */ 36 export 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 5 import {t} from '@lingui/macro' 6 7 import * as Toast from '#/view/com/util/Toast' 8 + import {IS_ANDROID, IS_IOS} from '#/env' 9 10 /** 11 * This function shares a URL using the native Share API if available, or copies it to the clipboard ··· 14 * clipboard. 15 */ 16 export 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 */ 36 export 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
··· 5 6 import {logger} from '#/logger' 7 import {type MetricEvents} from '#/logger/metrics' 8 - import {isWeb} from '#/platform/detection' 9 import * as persisted from '#/state/persisted' 10 import * as env from '#/env' 11 import {useSession} from '../../state/session' 12 import {timeout} from '../async/timeout' ··· 37 38 let refSrc = '' 39 let 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') ?? '')
··· 5 6 import {logger} from '#/logger' 7 import {type MetricEvents} from '#/logger/metrics' 8 import * as persisted from '#/state/persisted' 9 + import {IS_WEB} from '#/env' 10 import * as env from '#/env' 11 import {useSession} from '../../state/session' 12 import {timeout} from '../async/timeout' ··· 37 38 let refSrc = '' 39 let 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
··· 1 import {Dimensions} from 'react-native' 2 3 import {isSafari} from '#/lib/browser' 4 - import {isWeb} from '#/platform/detection' 5 6 const {height: SCREEN_HEIGHT} = Dimensions.get('window') 7 8 - 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 } 561 562 - if (isWeb) { 563 if (isSafari) { 564 id = id.replace('AAAAC', 'AAAP1') 565 filename = filename.replace('.gif', '.mp4')
··· 1 import {Dimensions} from 'react-native' 2 3 import {isSafari} from '#/lib/browser' 4 + import {IS_WEB} from '#/env' 5 6 const {height: SCREEN_HEIGHT} = Dimensions.get('window') 7 8 + 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 } 561 562 + 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' 7 8 - import {isWeb} from '#/platform/detection' 9 import {type Theme, type TypographyVariant} from './ThemeContext' 10 11 // 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' 7 8 + import {IS_WEB} from '#/env' 9 import {type Theme, type TypographyVariant} from './ThemeContext' 10 11 // 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' 15 import {enabledLogLevels} from '#/logger/util' 16 - import {isNative} from '#/platform/detection' 17 import {ENV} from '#/env' 18 19 export {type MetricEvents as Metrics} from '#/logger/metrics' ··· 21 const 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' 15 import {enabledLogLevels} from '#/logger/util' 16 + import {IS_NATIVE} from '#/env' 17 import {ENV} from '#/env' 18 19 export {type MetricEvents as Metrics} from '#/logger/metrics' ··· 21 const 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
··· 2 3 import {LogLevel, type Transport} from '#/logger/types' 4 import {prepareMetadata} from '#/logger/util' 5 - import {isWeb} from '#/platform/detection' 6 7 /** 8 * Used in dev mode to nicely log to the console ··· 33 msg += ` ${message.toString()}` 34 } 35 36 - if (isWeb) { 37 if (hasMetadata) { 38 console.groupCollapsed(msg) 39 console.log(metadata)
··· 2 3 import {LogLevel, type Transport} from '#/logger/types' 4 import {prepareMetadata} from '#/logger/util' 5 + import {IS_WEB} from '#/env' 6 7 /** 8 * Used in dev mode to nicely log to the console ··· 33 msg += ` ${message.toString()}` 34 } 35 36 + if (IS_WEB) { 37 if (hasMetadata) { 38 console.groupCollapsed(msg) 39 console.log(metadata)
-12
src/platform/detection.ts
··· 1 - import {Platform} from 'react-native' 2 - 3 - export const isIOS = Platform.OS === 'ios' 4 - export const isAndroid = Platform.OS === 'android' 5 - export const isNative = isIOS || isAndroid 6 - export const isWeb = !isNative 7 - export const isMobileWebMediaQuery = 'only screen and (max-width: 1300px)' 8 - export const isMobileWeb = 9 - isWeb && 10 - // @ts-ignore we know window exists -prf 11 - global.window.matchMedia(isMobileWebMediaQuery)?.matches 12 - export const isIPhoneWeb = isWeb && /iPhone/.test(navigator.userAgent)
···
-26
src/platform/urls.tsx
··· 1 - import {Linking} from 'react-native' 2 - 3 - import {isNative, isWeb} from './detection' 4 - 5 - export async function getInitialURL(): Promise<string | undefined> { 6 - if (isNative) { 7 - const url = await Linking.getInitialURL() 8 - if (url) { 9 - return url 10 - } 11 - return undefined 12 - } else { 13 - // @ts-ignore window exists -prf 14 - if (window.location.pathname !== '/') { 15 - return window.location.pathname 16 - } 17 - return undefined 18 - } 19 - } 20 - 21 - export function clearHash() { 22 - if (isWeb) { 23 - // @ts-ignore window exists -prf 24 - window.location.hash = '' 25 - } 26 - }
···
+2 -2
src/screens/Bookmarks/index.tsx
··· 21 type NativeStackScreenProps, 22 } from '#/lib/routes/types' 23 import {logger} from '#/logger' 24 - import {isIOS} from '#/platform/detection' 25 import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' 26 import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' 27 import {useSetMinimalShellMode} from '#/state/shell' ··· 38 import * as Skele from '#/components/Skeleton' 39 import * as toast from '#/components/Toast' 40 import {Text} from '#/components/Typography' 41 42 type 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' 23 import {logger} from '#/logger' 24 import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation' 25 import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery' 26 import {useSetMinimalShellMode} from '#/state/shell' ··· 37 import * as Skele from '#/components/Skeleton' 38 import * as toast from '#/components/Toast' 39 import {Text} from '#/components/Typography' 40 + import {IS_IOS} from '#/env' 41 42 type 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
··· 7 8 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 9 import {logger} from '#/logger' 10 - import {isWeb} from '#/platform/detection' 11 import { 12 type SessionAccount, 13 useAgent, ··· 24 import * as Layout from '#/components/Layout' 25 import {Loader} from '#/components/Loader' 26 import {Text} from '#/components/Typography' 27 28 const COL_WIDTH = 400 29 ··· 55 }, [setShowLoggedOut]) 56 57 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
··· 7 8 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 9 import {logger} from '#/logger' 10 import { 11 type SessionAccount, 12 useAgent, ··· 23 import * as Layout from '#/components/Layout' 24 import {Loader} from '#/components/Loader' 25 import {Text} from '#/components/Typography' 26 + import {IS_WEB} from '#/env' 27 28 const COL_WIDTH = 400 29 ··· 55 }, [setShowLoggedOut]) 56 57 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' 13 import {useSetMinimalShellMode} from '#/state/shell' 14 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 15 import {FindContactsFlow} from '#/components/contacts/FindContactsFlow' 16 import {useFindContactsFlowState} from '#/components/contacts/state' 17 import * as Layout from '#/components/Layout' 18 import {ScreenTransition} from '#/components/ScreenTransition' 19 20 type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'> 21 export function FindContactsFlowScreen({navigation}: Props) { ··· 48 49 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' 12 import {useSetMinimalShellMode} from '#/state/shell' 13 import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 14 import {FindContactsFlow} from '#/components/contacts/FindContactsFlow' 15 import {useFindContactsFlowState} from '#/components/contacts/state' 16 import * as Layout from '#/components/Layout' 17 import {ScreenTransition} from '#/components/ScreenTransition' 18 + import {IS_NATIVE} from '#/env' 19 20 type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'> 21 export function FindContactsFlowScreen({navigation}: Props) { ··· 48 49 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
··· 18 import {cleanError} from '#/lib/strings/errors' 19 import {createFullHandle} from '#/lib/strings/handles' 20 import {logger} from '#/logger' 21 - import {isIOS} from '#/platform/detection' 22 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 23 import {useSessionApi} from '#/state/session' 24 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 32 import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 33 import {Loader} from '#/components/Loader' 34 import {Text} from '#/components/Typography' 35 import {FormContainer} from './FormContainer' 36 37 type 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"
··· 18 import {cleanError} from '#/lib/strings/errors' 19 import {createFullHandle} from '#/lib/strings/handles' 20 import {logger} from '#/logger' 21 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 22 import {useSessionApi} from '#/state/session' 23 import {useLoggedOutViewControls} from '#/state/shell/logged-out' ··· 31 import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 32 import {Loader} from '#/components/Loader' 33 import {Text} from '#/components/Typography' 34 + import {IS_IOS} from '#/env' 35 import {FormContainer} from './FormContainer' 36 37 type 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
··· 13 import {type MessagesTabNavigatorParams} from '#/lib/routes/types' 14 import {cleanError} from '#/lib/strings/errors' 15 import {logger} from '#/logger' 16 - import {isNative} from '#/platform/detection' 17 import {listenSoftReset} from '#/state/events' 18 import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' 19 import {useMessagesEventBus} from '#/state/messages/events' ··· 38 import {Link} from '#/components/Link' 39 import {ListFooter} from '#/components/Lists' 40 import {Text} from '#/components/Typography' 41 import {ChatListItem} from './components/ChatListItem' 42 import {InboxPreview} from './components/InboxPreview' 43 ··· 222 223 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
··· 13 import {type MessagesTabNavigatorParams} from '#/lib/routes/types' 14 import {cleanError} from '#/lib/strings/errors' 15 import {logger} from '#/logger' 16 import {listenSoftReset} from '#/state/events' 17 import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' 18 import {useMessagesEventBus} from '#/state/messages/events' ··· 37 import {Link} from '#/components/Link' 38 import {ListFooter} from '#/components/Lists' 39 import {Text} from '#/components/Typography' 40 + import {IS_NATIVE} from '#/env' 41 import {ChatListItem} from './components/ChatListItem' 42 import {InboxPreview} from './components/InboxPreview' 43 ··· 222 223 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' 25 import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 26 import {useEmail} from '#/state/email-verification' 27 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' ··· 43 import {Error} from '#/components/Error' 44 import * as Layout from '#/components/Layout' 45 import {Loader} from '#/components/Loader' 46 47 type Props = NativeStackScreenProps< 48 CommonNavigatorParams, ··· 74 useCallback(() => { 75 setCurrentConvoId(convoId) 76 77 - if (isWeb && !gtMobile) { 78 setMinimalShellMode(true) 79 } else { 80 setMinimalShellMode(false)
··· 21 type CommonNavigatorParams, 22 type NavigationProp, 23 } from '#/lib/routes/types' 24 import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow' 25 import {useEmail} from '#/state/email-verification' 26 import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo' ··· 42 import {Error} from '#/components/Error' 43 import * as Layout from '#/components/Layout' 44 import {Loader} from '#/components/Loader' 45 + import {IS_WEB} from '#/env' 46 47 type Props = NativeStackScreenProps< 48 CommonNavigatorParams, ··· 74 useCallback(() => { 75 setCurrentConvoId(convoId) 76 77 + if (IS_WEB && !gtMobile) { 78 setMinimalShellMode(true) 79 } else { 80 setMinimalShellMode(false)
+2 -2
src/screens/Messages/Inbox.tsx
··· 21 } from '#/lib/routes/types' 22 import {cleanError} from '#/lib/strings/errors' 23 import {logger} from '#/logger' 24 - import {isNative} from '#/platform/detection' 25 import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' 26 import {useMessagesEventBus} from '#/state/messages/events' 27 import {useLeftConvos} from '#/state/queries/messages/leave-conversation' ··· 44 import * as Layout from '#/components/Layout' 45 import {ListFooter} from '#/components/Lists' 46 import {Text} from '#/components/Typography' 47 import {RequestListItem} from './components/RequestListItem' 48 49 type 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' 22 import {cleanError} from '#/lib/strings/errors' 23 import {logger} from '#/logger' 24 import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' 25 import {useMessagesEventBus} from '#/state/messages/events' 26 import {useLeftConvos} from '#/state/queries/messages/leave-conversation' ··· 43 import * as Layout from '#/components/Layout' 44 import {ListFooter} from '#/components/Lists' 45 import {Text} from '#/components/Typography' 46 + import {IS_NATIVE} from '#/env' 47 import {RequestListItem} from './components/RequestListItem' 48 49 type 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
··· 5 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 6 7 import {type CommonNavigatorParams} from '#/lib/routes/types' 8 - import {isNative} from '#/platform/detection' 9 import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' 10 import {useProfileQuery} from '#/state/queries/profile' 11 import {useSession} from '#/state/session' ··· 16 import * as Toggle from '#/components/forms/Toggle' 17 import * as Layout from '#/components/Layout' 18 import {Text} from '#/components/Typography' 19 import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 20 21 type 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]}>
··· 5 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 6 7 import {type CommonNavigatorParams} from '#/lib/routes/types' 8 import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration' 9 import {useProfileQuery} from '#/state/queries/profile' 10 import {useSession} from '#/state/session' ··· 15 import * as Toggle from '#/components/forms/Toggle' 16 import * as Layout from '#/components/Layout' 17 import {Text} from '#/components/Typography' 18 + import {IS_NATIVE} from '#/env' 19 import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 20 21 type 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' 24 import {useProfileShadow} from '#/state/cache/profile-shadow' 25 import {useModerationOpts} from '#/state/preferences/moderation-opts' 26 import { ··· 46 import {Text} from '#/components/Typography' 47 import {useSimpleVerificationState} from '#/components/verification' 48 import {VerificationCheck} from '#/components/verification/VerificationCheck' 49 import type * as bsky from '#/types/bsky' 50 51 export 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' 23 import {useProfileShadow} from '#/state/cache/profile-shadow' 24 import {useModerationOpts} from '#/state/preferences/moderation-opts' 25 import { ··· 45 import {Text} from '#/components/Typography' 46 import {useSimpleVerificationState} from '#/components/verification' 47 import {VerificationCheck} from '#/components/verification/VerificationCheck' 48 + import {IS_NATIVE} from '#/env' 49 import type * as bsky from '#/types/bsky' 50 51 export 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
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 - import {isIOS, isWeb} from '#/platform/detection' 22 import {useEmail} from '#/state/email-verification' 23 import { 24 useMessageDraft, ··· 29 import {android, atoms as a, useTheme} from '#/alf' 30 import {useSharedInputStyles} from '#/components/forms/TextField' 31 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 32 import {useExtractEmbedFromFacets} from './MessageInputEmbed' 33 34 const 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}
··· 18 19 import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20 import {useHaptics} from '#/lib/haptics' 21 import {useEmail} from '#/state/email-verification' 22 import { 23 useMessageDraft, ··· 28 import {android, atoms as a, useTheme} from '#/alf' 29 import {useSharedInputStyles} from '#/components/forms/TextField' 30 import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 31 + import {IS_IOS, IS_WEB} from '#/env' 32 import {useExtractEmbedFromFacets} from './MessageInputEmbed' 33 34 const 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' 26 import {logger} from '#/logger' 27 - import {isNative} from '#/platform/detection' 28 - import {isWeb} from '#/platform/detection' 29 import { 30 type ActiveConvoStates, 31 isConvoActive, ··· 52 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 53 import {Loader} from '#/components/Loader' 54 import {Text} from '#/components/Typography' 55 import {ChatStatusInfo} from './ChatStatusInfo' 56 import {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) 392 393 - 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> 470 471 - {isWeb && ( 472 <EmojiPicker 473 pinToTop 474 state={emojiPickerState}
··· 24 isBskyPostUrl, 25 } from '#/lib/strings/url-helpers' 26 import {logger} from '#/logger' 27 import { 28 type ActiveConvoStates, 29 isConvoActive, ··· 50 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 51 import {Loader} from '#/components/Loader' 52 import {Text} from '#/components/Typography' 53 + import {IS_NATIVE} from '#/env' 54 + import {IS_WEB} from '#/env' 55 import {ChatStatusInfo} from './ChatStatusInfo' 56 import {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) 392 393 + 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> 470 471 + {IS_WEB && ( 472 <EmojiPicker 473 pinToTop 474 state={emojiPickerState}
+2 -2
src/screens/Moderation/index.tsx
··· 11 type NativeStackScreenProps, 12 } from '#/lib/routes/types' 13 import {logger} from '#/logger' 14 - import {isIOS} from '#/platform/detection' 15 import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 16 import { 17 useMyLabelersQuery, ··· 45 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 46 import {Text} from '#/components/Typography' 47 import {useAgeAssurance} from '#/ageAssurance' 48 49 function 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 187 188 if (aa.flags.adultContentDisabled) {
··· 11 type NativeStackScreenProps, 12 } from '#/lib/routes/types' 13 import {logger} from '#/logger' 14 import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 15 import { 16 useMyLabelersQuery, ··· 44 import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' 45 import {Text} from '#/components/Typography' 46 import {useAgeAssurance} from '#/ageAssurance' 47 + import {IS_IOS} from '#/env' 48 49 function 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 187 188 if (aa.flags.adultContentDisabled) {
+5 -5
src/screens/Onboarding/Layout.tsx
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 - import {isAndroid, isWeb} from '#/platform/detection' 8 import {useOnboardingDispatch} from '#/state/shell' 9 import {useOnboardingInternalState} from '#/screens/Onboarding/state' 10 import { ··· 21 import {HEADER_SLOT_SIZE} from '#/components/Layout' 22 import {createPortalGroup} from '#/components/Portal' 23 import {P, Text} from '#/components/Typography' 24 import {IS_INTERNAL} from '#/env' 25 26 const ONBOARDING_COL_WIDTH = 420 ··· 58 aria-label={dialogLabel} 59 accessibilityLabel={dialogLabel} 60 accessibilityHint={_(msg`Customizes your Bluesky experience`)} 61 - style={[isWeb ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 62 {!gtMobile ? ( 63 <View 64 style={[ ··· 151 paddingTop: gtMobile ? 40 : headerHeight, 152 paddingBottom: footerHeight, 153 }} 154 - showsVerticalScrollIndicator={!isAndroid} 155 scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}} 156 // @ts-expect-error web only --prf 157 dataSet={{'stable-gutters': 1}} ··· 167 <View 168 onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)} 169 style={[ 170 - isWeb ? a.fixed : a.absolute, 171 {bottom: 0, left: 0, right: 0}, 172 t.atoms.bg, 173 t.atoms.border_contrast_low, 174 a.border_t, 175 a.align_center, 176 gtMobile ? a.px_5xl : a.px_xl, 177 - isWeb 178 ? a.py_2xl 179 : { 180 paddingTop: tokens.space.md,
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {useOnboardingDispatch} from '#/state/shell' 8 import {useOnboardingInternalState} from '#/screens/Onboarding/state' 9 import { ··· 20 import {HEADER_SLOT_SIZE} from '#/components/Layout' 21 import {createPortalGroup} from '#/components/Portal' 22 import {P, Text} from '#/components/Typography' 23 + import {IS_ANDROID, IS_WEB} from '#/env' 24 import {IS_INTERNAL} from '#/env' 25 26 const ONBOARDING_COL_WIDTH = 420 ··· 58 aria-label={dialogLabel} 59 accessibilityLabel={dialogLabel} 60 accessibilityHint={_(msg`Customizes your Bluesky experience`)} 61 + style={[IS_WEB ? a.fixed : a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}> 62 {!gtMobile ? ( 63 <View 64 style={[ ··· 151 paddingTop: gtMobile ? 40 : headerHeight, 152 paddingBottom: footerHeight, 153 }} 154 + showsVerticalScrollIndicator={!IS_ANDROID} 155 scrollIndicatorInsets={{bottom: footerHeight - insets.bottom}} 156 // @ts-expect-error web only --prf 157 dataSet={{'stable-gutters': 1}} ··· 167 <View 168 onLayout={evt => setFooterHeight(evt.nativeEvent.layout.height)} 169 style={[ 170 + IS_WEB ? a.fixed : a.absolute, 171 {bottom: 0, left: 0, right: 0}, 172 t.atoms.bg, 173 t.atoms.border_contrast_low, 174 a.border_t, 175 a.align_center, 176 gtMobile ? a.px_5xl : a.px_xl, 177 + IS_WEB 178 ? a.py_2xl 179 : { 180 paddingTop: tokens.space.md,
+2 -2
src/screens/Onboarding/StepFinished/index.tsx
··· 22 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 23 import {logEvent, useGate} from '#/lib/statsig/statsig' 24 import {logger} from '#/logger' 25 - import {isWeb} from '#/platform/detection' 26 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 27 import {getAllListMembers} from '#/state/queries/list-members' 28 import {preferencesQueryKey} from '#/state/queries/preferences' ··· 47 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 48 import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' 49 import {Loader} from '#/components/Loader' 50 import * as bsky from '#/types/bsky' 51 import {ValuePropositionPager} from './ValuePropositionPager' 52 ··· 305 306 <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"
··· 22 import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 23 import {logEvent, useGate} from '#/lib/statsig/statsig' 24 import {logger} from '#/logger' 25 import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 26 import {getAllListMembers} from '#/state/queries/list-members' 27 import {preferencesQueryKey} from '#/state/queries/preferences' ··· 46 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 47 import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' 48 import {Loader} from '#/components/Loader' 49 + import {IS_WEB} from '#/env' 50 import * as bsky from '#/types/bsky' 51 import {ValuePropositionPager} from './ValuePropositionPager' 52 ··· 305 306 <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
··· 17 import {logEvent, useGate} from '#/lib/statsig/statsig' 18 import {isCancelledError} from '#/lib/strings/errors' 19 import {logger} from '#/logger' 20 - import {isNative, isWeb} from '#/platform/detection' 21 import { 22 OnboardingControls, 23 OnboardingDescriptionText, ··· 38 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 39 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' 40 import {Text} from '#/components/Typography' 41 import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types' 42 43 export interface Avatar { ··· 183 let image = items[0] 184 if (!image) return 185 186 - if (!isWeb) { 187 try { 188 image = await openCropper({ 189 imageUri: image.path, ··· 200 201 // 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
··· 17 import {logEvent, useGate} from '#/lib/statsig/statsig' 18 import {isCancelledError} from '#/lib/strings/errors' 19 import {logger} from '#/logger' 20 import { 21 OnboardingControls, 22 OnboardingDescriptionText, ··· 37 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 38 import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo' 39 import {Text} from '#/components/Typography' 40 + import {IS_NATIVE, IS_WEB} from '#/env' 41 import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types' 42 43 export interface Avatar { ··· 183 let image = items[0] 184 if (!image) return 185 186 + if (!IS_WEB) { 187 try { 188 image = await openCropper({ 189 imageUri: image.path, ··· 200 201 // 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
+5 -5
src/screens/Onboarding/StepSuggestedAccounts/index.tsx
··· 10 import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 11 import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 12 import {logger} from '#/logger' 13 - import {isWeb} from '#/platform/detection' 14 import {updateProfileShadow} from '#/state/cache/profile-shadow' 15 import {useLanguagePrefs} from '#/state/preferences' 16 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 31 import {Loader} from '#/components/Loader' 32 import * as ProfileCard from '#/components/ProfileCard' 33 import * as toast from '#/components/Toast' 34 import type * as bsky from '#/types/bsky' 35 import {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 352 353 - if (isWeb && typeof IntersectionObserver !== 'undefined') { 354 const observer = new IntersectionObserver( 355 entries => { 356 if (entries[0]?.isIntersecting && !hasTrackedRef.current) {
··· 10 import {popularInterests, useInterestsDisplayNames} from '#/lib/interests' 11 import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 12 import {logger} from '#/logger' 13 import {updateProfileShadow} from '#/state/cache/profile-shadow' 14 import {useLanguagePrefs} from '#/state/preferences' 15 import {useModerationOpts} from '#/state/preferences/moderation-opts' ··· 30 import {Loader} from '#/components/Loader' 31 import * as ProfileCard from '#/components/ProfileCard' 32 import * as toast from '#/components/Toast' 33 + import {IS_WEB} from '#/env' 34 import type * as bsky from '#/types/bsky' 35 import {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 352 353 + 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
··· 4 5 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 6 import {useGate} from '#/lib/statsig/statsig' 7 - import {isNative} from '#/platform/detection' 8 import {useLanguagePrefs} from '#/state/preferences' 9 import { 10 Layout, ··· 24 import {useFindContactsFlowState} from '#/components/contacts/state' 25 import {Portal} from '#/components/Portal' 26 import {ScreenTransition} from '#/components/ScreenTransition' 27 import {ENV} from '#/env' 28 import {StepFindContacts} from './StepFindContacts' 29 import {StepFindContactsIntro} from './StepFindContactsIntro' ··· 50 useIsFindContactsFeatureEnabledBasedOnGeolocation() 51 const showFindContacts = 52 ENV !== 'e2e' && 53 - isNative && 54 findContactsEnabled && 55 !gate('disable_onboarding_find_contacts') 56
··· 4 5 import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 6 import {useGate} from '#/lib/statsig/statsig' 7 import {useLanguagePrefs} from '#/state/preferences' 8 import { 9 Layout, ··· 23 import {useFindContactsFlowState} from '#/components/contacts/state' 24 import {Portal} from '#/components/Portal' 25 import {ScreenTransition} from '#/components/ScreenTransition' 26 + import {IS_NATIVE} from '#/env' 27 import {ENV} from '#/env' 28 import {StepFindContacts} from './StepFindContacts' 29 import {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
··· 3 import {PrivacySensitive} from 'expo-privacy-sensitive' 4 5 import {useAppState} from '#/lib/hooks/useAppState' 6 - import {isIOS} from '#/platform/detection' 7 import {atoms as a, useTheme} from '#/alf' 8 import {sizes as iconSizes} from '#/components/icons/common' 9 import {Mark as Logo} from '#/components/icons/Logo' 10 11 const ICON_SIZE = 'xl' as const 12 ··· 25 26 const appState = useAppState() 27 28 - if (!isIOS || appState !== 'active') return children 29 30 return ( 31 <View
··· 3 import {PrivacySensitive} from 'expo-privacy-sensitive' 4 5 import {useAppState} from '#/lib/hooks/useAppState' 6 import {atoms as a, useTheme} from '#/alf' 7 import {sizes as iconSizes} from '#/components/icons/common' 8 import {Mark as Logo} from '#/components/icons/Logo' 9 + import {IS_IOS} from '#/env' 10 11 const ICON_SIZE = 'xl' as const 12 ··· 25 26 const appState = useAppState() 27 28 + if (!IS_IOS || appState !== 'active') return children 29 30 return ( 31 <View
+2 -2
src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx
··· 5 import {useNavigation} from '@react-navigation/native' 6 7 import {logger} from '#/logger' 8 - import {isIOS} from '#/platform/detection' 9 import {useProfileShadow} from '#/state/cache/profile-shadow' 10 import { 11 useProfileFollowMutationQueue, ··· 17 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 18 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 19 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 20 import {GrowthHack} from './GrowthHack' 21 22 export function ThreadItemAnchorFollowButton({did}: {did: string}) { 23 - if (isIOS) { 24 return ( 25 <GrowthHack> 26 <ThreadItemAnchorFollowButtonInner did={did} />
··· 5 import {useNavigation} from '@react-navigation/native' 6 7 import {logger} from '#/logger' 8 import {useProfileShadow} from '#/state/cache/profile-shadow' 9 import { 10 useProfileFollowMutationQueue, ··· 16 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17 import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 18 import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19 + import {IS_IOS} from '#/env' 20 import {GrowthHack} from './GrowthHack' 21 22 export 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' 8 import type React from 'react' 9 10 - import {isIOS} from '#/platform/detection' 11 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 12 13 export function GrowableAvatar({ 14 children, ··· 20 const pagerContext = usePagerHeaderContext() 21 22 // 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' 8 import type React from 'react' 9 10 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 11 + import {IS_IOS} from '#/env' 12 13 export function GrowableAvatar({ 14 children, ··· 20 const pagerContext = usePagerHeaderContext() 21 22 // 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
··· 15 import {useIsFetching} from '@tanstack/react-query' 16 import type React from 'react' 17 18 - import {isIOS} from '#/platform/detection' 19 import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs' 20 import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed' 21 import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens' 22 import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists' 23 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 24 import {atoms as a} from '#/alf' 25 26 const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) 27 ··· 39 const pagerContext = usePagerHeaderContext() 40 41 // 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 ]}>
··· 15 import {useIsFetching} from '@tanstack/react-query' 16 import type React from 'react' 17 18 import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs' 19 import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed' 20 import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens' 21 import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists' 22 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 23 import {atoms as a} from '#/alf' 24 + import {IS_IOS} from '#/env' 25 26 const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) 27 ··· 39 const pagerContext = usePagerHeaderContext() 40 41 // 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
··· 4 import {useLingui} from '@lingui/react' 5 6 import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 7 - import {isIOS, isNative} from '#/platform/detection' 8 import {type Shadow} from '#/state/cache/types' 9 import {atoms as a, useTheme, web} from '#/alf' 10 import {NewskieDialog} from '#/components/NewskieDialog' 11 import {Text} from '#/components/Typography' 12 13 export 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>
··· 4 import {useLingui} from '@lingui/react' 5 6 import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 7 import {type Shadow} from '#/state/cache/types' 8 import {atoms as a, useTheme, web} from '#/alf' 9 import {NewskieDialog} from '#/components/NewskieDialog' 10 import {Text} from '#/components/Typography' 11 + import {IS_IOS, IS_NATIVE} from '#/env' 12 13 export 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>
+3 -3
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 15 import {useHaptics} from '#/lib/haptics' 16 import {isAppLabeler} from '#/lib/moderation' 17 import {logger} from '#/logger' 18 - import {isIOS} from '#/platform/detection' 19 import {useProfileShadow} from '#/state/cache/profile-shadow' 20 import {type Shadow} from '#/state/cache/types' 21 import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' ··· 35 import {RichText} from '#/components/RichText' 36 import * as Toast from '#/components/Toast' 37 import {Text} from '#/components/Typography' 38 import {ProfileHeaderDisplayName} from './DisplayName' 39 import {EditProfileDialog} from './EditProfileDialog' 40 import {ProfileHeaderHandle} from './Handle' ··· 111 isPlaceholderProfile={isPlaceholderProfile}> 112 <View 113 style={[a.px_lg, a.pt_md, a.pb_sm]} 114 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 115 <View 116 style={[a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg]} 117 - pointerEvents={isIOS ? 'auto' : 'box-none'}> 118 <HeaderLabelerButtons profile={profile} /> 119 </View> 120 <View style={[a.flex_col, a.gap_2xs, a.pt_2xs, a.pb_md]}>
··· 15 import {useHaptics} from '#/lib/haptics' 16 import {isAppLabeler} from '#/lib/moderation' 17 import {logger} from '#/logger' 18 import {useProfileShadow} from '#/state/cache/profile-shadow' 19 import {type Shadow} from '#/state/cache/types' 20 import {useLabelerSubscriptionMutation} from '#/state/queries/labeler' ··· 34 import {RichText} from '#/components/RichText' 35 import * as Toast from '#/components/Toast' 36 import {Text} from '#/components/Typography' 37 + import {IS_IOS} from '#/env' 38 import {ProfileHeaderDisplayName} from './DisplayName' 39 import {EditProfileDialog} from './EditProfileDialog' 40 import {ProfileHeaderHandle} from './Handle' ··· 111 isPlaceholderProfile={isPlaceholderProfile}> 112 <View 113 style={[a.px_lg, a.pt_md, a.pb_sm]} 114 + pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 115 <View 116 style={[a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg]} 117 + pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 118 <HeaderLabelerButtons profile={profile} /> 119 </View> 120 <View style={[a.flex_col, a.gap_2xs, a.pt_2xs, a.pb_md]}>
+3 -3
src/screens/Profile/Header/ProfileHeaderStandard.tsx
··· 15 import {sanitizeDisplayName} from '#/lib/strings/display-names' 16 import {sanitizeHandle} from '#/lib/strings/handles' 17 import {logger} from '#/logger' 18 - import {isIOS} from '#/platform/detection' 19 import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' 20 import { 21 useProfileBlockMutationQueue, ··· 39 import * as Toast from '#/components/Toast' 40 import {Text} from '#/components/Typography' 41 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 42 import {EditProfileDialog} from './EditProfileDialog' 43 import {ProfileHeaderHandle} from './Handle' 44 import {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}
··· 15 import {sanitizeDisplayName} from '#/lib/strings/display-names' 16 import {sanitizeHandle} from '#/lib/strings/handles' 17 import {logger} from '#/logger' 18 import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' 19 import { 20 useProfileBlockMutationQueue, ··· 38 import * as Toast from '#/components/Toast' 39 import {Text} from '#/components/Typography' 40 import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton' 41 + import {IS_IOS} from '#/env' 42 import {EditProfileDialog} from './EditProfileDialog' 43 import {ProfileHeaderHandle} from './Handle' 44 import {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
··· 19 import {useHaptics} from '#/lib/haptics' 20 import {type NavigationProp} from '#/lib/routes/types' 21 import {logger} from '#/logger' 22 - import {isIOS} from '#/platform/detection' 23 import {type Shadow} from '#/state/cache/types' 24 import {useLightboxControls} from '#/state/lightbox' 25 import {useSession} from '#/state/session' ··· 35 import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' 36 import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' 37 import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' 38 import {GrowableAvatar} from './GrowableAvatar' 39 import {GrowableBanner} from './GrowableBanner' 40 import {StatusBarShadow} from './StatusBarShadow' ··· 167 }, [profile.banner, moderation, _openLightbox, bannerRef]) 168 169 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 ))}
··· 19 import {useHaptics} from '#/lib/haptics' 20 import {type NavigationProp} from '#/lib/routes/types' 21 import {logger} from '#/logger' 22 import {type Shadow} from '#/state/cache/types' 23 import {useLightboxControls} from '#/state/lightbox' 24 import {useSession} from '#/state/session' ··· 34 import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' 35 import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' 36 import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' 37 + import {IS_IOS} from '#/env' 38 import {GrowableAvatar} from './GrowableAvatar' 39 import {GrowableBanner} from './GrowableBanner' 40 import {StatusBarShadow} from './StatusBarShadow' ··· 167 }, [profile.banner, moderation, _openLightbox, bannerRef]) 168 169 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
··· 5 import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 import {LinearGradient} from 'expo-linear-gradient' 7 8 - import {isIOS} from '#/platform/detection' 9 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 10 import {atoms as a} from '#/alf' 11 12 const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient) 13 ··· 15 const {top: topInset} = useSafeAreaInsets() 16 const pagerContext = usePagerHeaderContext() 17 18 - if (isIOS && pagerContext) { 19 const {scrollY} = pagerContext 20 return <StatusBarShadowInnner scrollY={scrollY} /> 21 }
··· 5 import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 import {LinearGradient} from 'expo-linear-gradient' 7 8 import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' 9 import {atoms as a} from '#/alf' 10 + import {IS_IOS} from '#/env' 11 12 const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient) 13 ··· 15 const {top: topInset} = useSafeAreaInsets() 16 const pagerContext = usePagerHeaderContext() 17 18 + if (IS_IOS && pagerContext) { 19 const {scrollY} = pagerContext 20 return <StatusBarShadowInnner scrollY={scrollY} /> 21 }
+2 -2
src/screens/Profile/Header/SuggestedFollows.tsx
··· 2 import {type AppBskyActorDefs} from '@atproto/api' 3 4 import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 5 - import {isAndroid} from '#/platform/detection' 6 import {useModerationOpts} from '#/state/preferences/moderation-opts' 7 import { 8 useSuggestedFollowsByActorQuery, ··· 10 } from '#/state/queries/suggested-follows' 11 import {useBreakpoints} from '#/alf' 12 import {ProfileGrid} from '#/components/FeedInterstitials' 13 14 const 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 214 215 return ( 216 <AccordionAnimation isExpanded={isExpanded}>
··· 2 import {type AppBskyActorDefs} from '@atproto/api' 3 4 import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 5 import {useModerationOpts} from '#/state/preferences/moderation-opts' 6 import { 7 useSuggestedFollowsByActorQuery, ··· 9 } from '#/state/queries/suggested-follows' 10 import {useBreakpoints} from '#/alf' 11 import {ProfileGrid} from '#/components/FeedInterstitials' 12 + import {IS_ANDROID} from '#/env' 13 14 const 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 214 215 return ( 216 <AccordionAnimation isExpanded={isExpanded}>
+2 -2
src/screens/Profile/Header/index.tsx
··· 17 import {useIsFocused} from '@react-navigation/native' 18 19 import {sanitizeHandle} from '#/lib/strings/handles' 20 - import {isNative} from '#/platform/detection' 21 import {useProfileShadow} from '#/state/cache/profile-shadow' 22 import {useModerationOpts} from '#/state/preferences/moderation-opts' 23 import {useSetLightStatusBar} from '#/state/shell/light-status-bar' ··· 26 import {atoms as a, useTheme} from '#/alf' 27 import {Header} from '#/components/Layout' 28 import * as ProfileCard from '#/components/ProfileCard' 29 import { 30 HeaderLabelerButtons, 31 ProfileHeaderLabeler, ··· 83 84 return ( 85 <> 86 - {isNative && ( 87 <MinimalHeader 88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)} 89 profile={props.profile}
··· 17 import {useIsFocused} from '@react-navigation/native' 18 19 import {sanitizeHandle} from '#/lib/strings/handles' 20 import {useProfileShadow} from '#/state/cache/profile-shadow' 21 import {useModerationOpts} from '#/state/preferences/moderation-opts' 22 import {useSetLightStatusBar} from '#/state/shell/light-status-bar' ··· 25 import {atoms as a, useTheme} from '#/alf' 26 import {Header} from '#/components/Layout' 27 import * as ProfileCard from '#/components/ProfileCard' 28 + import {IS_NATIVE} from '#/env' 29 import { 30 HeaderLabelerButtons, 31 ProfileHeaderLabeler, ··· 83 84 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
··· 17 import {type NavigationProp} from '#/lib/routes/types' 18 import {makeRecordUri} from '#/lib/strings/url-helpers' 19 import {s} from '#/lib/styles' 20 - import {isNative} from '#/platform/detection' 21 import {listenSoftReset} from '#/state/events' 22 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 23 import { ··· 47 } from '#/screens/Profile/components/ProfileFeedHeader' 48 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 49 import * as Layout from '#/components/Layout' 50 51 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> 52 export function ProfileFeedScreen(props: Props) { ··· 175 176 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]) 209 210 return (
··· 17 import {type NavigationProp} from '#/lib/routes/types' 18 import {makeRecordUri} from '#/lib/strings/url-helpers' 19 import {s} from '#/lib/styles' 20 import {listenSoftReset} from '#/state/events' 21 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 22 import { ··· 46 } from '#/screens/Profile/components/ProfileFeedHeader' 47 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 48 import * as Layout from '#/components/Layout' 49 + import {IS_NATIVE} from '#/env' 50 51 type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'> 52 export function ProfileFeedScreen(props: Props) { ··· 175 176 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]) 209 210 return (
+4 -4
src/screens/Profile/Sections/Feed.tsx
··· 5 import {useQueryClient} from '@tanstack/react-query' 6 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8 - import {isIOS, isNative} from '#/platform/detection' 9 import { 10 type FeedDescriptor, 11 RQKEY as FEED_RQKEY, ··· 21 import {atoms as a, ios, useTheme} from '#/alf' 22 import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' 23 import {Text} from '#/components/Typography' 24 import {type SectionRef} from './types' 25 26 interface 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]) 86 87 useEffect(() => { 88 - if (isIOS && isFocused && scrollElRef.current) { 89 const nativeTag = findNodeHandle(scrollElRef.current) 90 setScrollViewTag(nativeTag) 91 }
··· 5 import {useQueryClient} from '@tanstack/react-query' 6 7 import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 8 import { 9 type FeedDescriptor, 10 RQKEY as FEED_RQKEY, ··· 20 import {atoms as a, ios, useTheme} from '#/alf' 21 import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' 22 import {Text} from '#/components/Typography' 23 + import {IS_IOS, IS_NATIVE} from '#/env' 24 import {type SectionRef} from './types' 25 26 interface 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]) 86 87 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
··· 10 import {useLingui} from '@lingui/react' 11 12 import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' 13 - import {isIOS, isNative} from '#/platform/detection' 14 import {List, type ListRef} from '#/view/com/util/List' 15 import {atoms as a, ios, tokens, useTheme} from '#/alf' 16 import {Divider} from '#/components/Divider' ··· 19 import {Loader} from '#/components/Loader' 20 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 21 import {Text} from '#/components/Typography' 22 import {ErrorState} from '../ErrorState' 23 import {type SectionRef} from './types' 24 ··· 49 50 const onScrollToTop = useCallback(() => { 51 scrollElRef.current?.scrollToOffset({ 52 - animated: isNative, 53 offset: -headerHeight, 54 }) 55 }, [scrollElRef, headerHeight]) ··· 59 })) 60 61 useEffect(() => { 62 - if (isIOS && isFocused && scrollElRef.current) { 63 const nativeTag = findNodeHandle(scrollElRef.current) 64 setScrollViewTag(nativeTag) 65 }
··· 10 import {useLingui} from '@lingui/react' 11 12 import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' 13 import {List, type ListRef} from '#/view/com/util/List' 14 import {atoms as a, ios, tokens, useTheme} from '#/alf' 15 import {Divider} from '#/components/Divider' ··· 18 import {Loader} from '#/components/Loader' 19 import {LabelerLabelPreference} from '#/components/moderation/LabelPreference' 20 import {Text} from '#/components/Typography' 21 + import {IS_IOS, IS_NATIVE} from '#/env' 22 import {ErrorState} from '../ErrorState' 23 import {type SectionRef} from './types' 24 ··· 49 50 const onScrollToTop = useCallback(() => { 51 scrollElRef.current?.scrollToOffset({ 52 + animated: IS_NATIVE, 53 offset: -headerHeight, 54 }) 55 }, [scrollElRef, headerHeight]) ··· 59 })) 60 61 useEffect(() => { 62 + if (IS_IOS && isFocused && scrollElRef.current) { 63 const nativeTag = findNodeHandle(scrollElRef.current) 64 setScrollViewTag(nativeTag) 65 }
+3 -3
src/screens/Profile/components/ProfileFeedHeader.tsx
··· 11 import {sanitizeHandle} from '#/lib/strings/handles' 12 import {toShareUrl} from '#/lib/strings/url-helpers' 13 import {logger} from '#/logger' 14 - import {isWeb} from '#/platform/detection' 15 import {type FeedSourceFeedInfo} from '#/state/queries/feed' 16 import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' 17 import { ··· 52 } from '#/components/moderation/ReportDialog' 53 import {RichText} from '#/components/RichText' 54 import {Text} from '#/components/Typography' 55 56 export function ProfileFeedHeaderSkeleton() { 57 const t = useTheme() ··· 192 style={[ 193 a.justify_start, 194 { 195 - paddingVertical: isWeb ? 2 : 4, 196 paddingRight: 8, 197 }, 198 ]} ··· 211 t.atoms.bg_contrast_25, 212 { 213 opacity: 0, 214 - left: isWeb ? -2 : -4, 215 right: 0, 216 }, 217 pressed && {
··· 11 import {sanitizeHandle} from '#/lib/strings/handles' 12 import {toShareUrl} from '#/lib/strings/url-helpers' 13 import {logger} from '#/logger' 14 import {type FeedSourceFeedInfo} from '#/state/queries/feed' 15 import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like' 16 import { ··· 51 } from '#/components/moderation/ReportDialog' 52 import {RichText} from '#/components/RichText' 53 import {Text} from '#/components/Typography' 54 + import {IS_WEB} from '#/env' 55 56 export function ProfileFeedHeaderSkeleton() { 57 const t = useTheme() ··· 192 style={[ 193 a.justify_start, 194 { 195 + paddingVertical: IS_WEB ? 2 : 4, 196 paddingRight: 8, 197 }, 198 ]} ··· 211 t.atoms.bg_contrast_25, 212 { 213 opacity: 0, 214 + left: IS_WEB ? -2 : -4, 215 right: 0, 216 }, 217 pressed && {
+2 -2
src/screens/ProfileList/AboutSection.tsx
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 - import {isNative} from '#/platform/detection' 8 import {useSession} from '#/state/session' 9 import {ListMembers} from '#/view/com/lists/ListMembers' 10 import {EmptyState} from '#/view/com/util/EmptyState' ··· 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 16 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 17 18 interface SectionRef { 19 scrollToTop: () => void ··· 42 43 const onScrollToTop = useCallback(() => { 44 scrollElRef.current?.scrollToOffset({ 45 - animated: isNative, 46 offset: -headerHeight, 47 }) 48 }, [scrollElRef, headerHeight])
··· 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {useSession} from '#/state/session' 8 import {ListMembers} from '#/view/com/lists/ListMembers' 9 import {EmptyState} from '#/view/com/util/EmptyState' ··· 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 15 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 16 + import {IS_NATIVE} from '#/env' 17 18 interface SectionRef { 19 scrollToTop: () => void ··· 42 43 const onScrollToTop = useCallback(() => { 44 scrollElRef.current?.scrollToOffset({ 45 + animated: IS_NATIVE, 46 offset: -headerHeight, 47 }) 48 }, [scrollElRef, headerHeight])
+2 -2
src/screens/ProfileList/FeedSection.tsx
··· 5 import {useIsFocused} from '@react-navigation/native' 6 import {useQueryClient} from '@tanstack/react-query' 7 8 - import {isNative} from '#/platform/detection' 9 import {listenSoftReset} from '#/state/events' 10 import { 11 type FeedDescriptor, ··· 19 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 20 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 21 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 22 23 interface SectionRef { 24 scrollToTop: () => void ··· 51 52 const onScrollToTop = useCallback(() => { 53 scrollElRef.current?.scrollToOffset({ 54 - animated: isNative, 55 offset: -headerHeight, 56 }) 57 queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
··· 5 import {useIsFocused} from '@react-navigation/native' 6 import {useQueryClient} from '@tanstack/react-query' 7 8 import {listenSoftReset} from '#/state/events' 9 import { 10 type FeedDescriptor, ··· 18 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 19 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 20 import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person' 21 + import {IS_NATIVE} from '#/env' 22 23 interface SectionRef { 24 scrollToTop: () => void ··· 51 52 const onScrollToTop = useCallback(() => { 53 scrollElRef.current?.scrollToOffset({ 54 + animated: IS_NATIVE, 55 offset: -headerHeight, 56 }) 57 queryClient.resetQueries({queryKey: FEED_RQKEY(feed)})
+4 -4
src/screens/ProfileList/components/MoreOptionsMenu.tsx
··· 7 import {shareUrl} from '#/lib/sharing' 8 import {toShareUrl} from '#/lib/strings/url-helpers' 9 import {logger} from '#/logger' 10 - import {isWeb} from '#/platform/detection' 11 import { 12 useListBlockMutation, 13 useListDeleteMutation, ··· 34 } from '#/components/moderation/ReportDialog' 35 import * as Prompt from '#/components/Prompt' 36 import * as Toast from '#/components/Toast' 37 38 export function MoreOptionsMenu({ 39 list, ··· 162 <Menu.Outer> 163 <Menu.Group> 164 <Menu.Item 165 - label={isWeb ? _(msg`Copy link to list`) : _(msg`Share via...`)} 166 onPress={onPressShare}> 167 <Menu.ItemText> 168 - {isWeb ? ( 169 <Trans>Copy link to list</Trans> 170 ) : ( 171 <Trans>Share via...</Trans> ··· 173 </Menu.ItemText> 174 <Menu.ItemIcon 175 position="right" 176 - icon={isWeb ? ChainLink : ShareIcon} 177 /> 178 </Menu.Item> 179 {savedFeedConfig && (
··· 7 import {shareUrl} from '#/lib/sharing' 8 import {toShareUrl} from '#/lib/strings/url-helpers' 9 import {logger} from '#/logger' 10 import { 11 useListBlockMutation, 12 useListDeleteMutation, ··· 33 } from '#/components/moderation/ReportDialog' 34 import * as Prompt from '#/components/Prompt' 35 import * as Toast from '#/components/Toast' 36 + import {IS_WEB} from '#/env' 37 38 export function MoreOptionsMenu({ 39 list, ··· 162 <Menu.Outer> 163 <Menu.Group> 164 <Menu.Item 165 + label={IS_WEB ? _(msg`Copy link to list`) : _(msg`Share via...`)} 166 onPress={onPressShare}> 167 <Menu.ItemText> 168 + {IS_WEB ? ( 169 <Trans>Copy link to list</Trans> 170 ) : ( 171 <Trans>Share via...</Trans> ··· 173 </Menu.ItemText> 174 <Menu.ItemIcon 175 position="right" 176 + icon={IS_WEB ? ChainLink : ShareIcon} 177 /> 178 </Menu.Item> 179 {savedFeedConfig && (
+11 -11
src/screens/Search/Shell.tsx
··· 22 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 23 import {MagnifyingGlassIcon} from '#/lib/icons' 24 import {type NavigationProp} from '#/lib/routes/types' 25 - import {isWeb} from '#/platform/detection' 26 import {listenSoftReset} from '#/state/events' 27 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 28 import { ··· 41 import {SearchInput} from '#/components/forms/SearchInput' 42 import * as Layout from '#/components/Layout' 43 import {Text} from '#/components/Typography' 44 import {account, useStorage} from '#/storage' 45 import type * as bsky from '#/types/bsky' 46 import {AutocompleteResults} from './components/AutocompleteResults' ··· 141 const [headerHeight, setHeaderHeight] = useState(0) 142 const headerRef = useRef(null) 143 useLayoutEffect(() => { 144 - if (isWeb) { 145 if (!headerRef.current) return 146 const measurement = (headerRef.current as Element).getBoundingClientRect() 147 setHeaderHeight(measurement.height) ··· 150 151 useFocusEffect( 152 useNonReactiveCallback(() => { 153 - if (isWeb) { 154 setSearchText(queryParam) 155 } 156 }), ··· 173 setShowAutocomplete(false) 174 updateSearchHistory(item) 175 176 - if (isWeb) { 177 // @ts-expect-error route is not typesafe 178 navigation.push(route.name, {...route.params, q: item}) 179 } else { ··· 188 scrollToTopWeb() 189 textInput.current?.blur() 190 setShowAutocomplete(false) 191 - if (isWeb) { 192 // Empty params resets the URL to be /search rather than /search?q= 193 // Also clear the tab parameter 194 const { ··· 211 }, [navigateToItem, searchText]) 212 213 const onAutocompleteResultPress = useCallback(() => { 214 - if (isWeb) { 215 setShowAutocomplete(false) 216 } else { 217 textInput.current?.blur() ··· 238 ) 239 240 const onSoftReset = useCallback(() => { 241 - if (isWeb) { 242 // Empty params resets the URL to be /search rather than /search?q= 243 // Also clear the tab parameter when soft resetting 244 const { ··· 265 ) 266 267 const onSearchInputFocus = useCallback(() => { 268 - if (isWeb) { 269 // Prevent a jump on iPad by ensuring that 270 // the initial focused render has no result list. 271 requestAnimationFrame(() => { ··· 282 283 // If a tab is specified, set the tab parameter 284 if (tab) { 285 - if (isWeb) { 286 navigation.setParams({...route.params, tab}) 287 } else { 288 navigation.setParams({tab}) ··· 299 <View 300 ref={headerRef} 301 onLayout={evt => { 302 - if (isWeb) setHeaderHeight(evt.nativeEvent.layout.height) 303 }} 304 style={[ 305 a.relative, ··· 581 } 582 583 function scrollToTopWeb() { 584 - if (isWeb) { 585 window.scrollTo(0, 0) 586 } 587 }
··· 22 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 23 import {MagnifyingGlassIcon} from '#/lib/icons' 24 import {type NavigationProp} from '#/lib/routes/types' 25 import {listenSoftReset} from '#/state/events' 26 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 27 import { ··· 40 import {SearchInput} from '#/components/forms/SearchInput' 41 import * as Layout from '#/components/Layout' 42 import {Text} from '#/components/Typography' 43 + import {IS_WEB} from '#/env' 44 import {account, useStorage} from '#/storage' 45 import type * as bsky from '#/types/bsky' 46 import {AutocompleteResults} from './components/AutocompleteResults' ··· 141 const [headerHeight, setHeaderHeight] = useState(0) 142 const headerRef = useRef(null) 143 useLayoutEffect(() => { 144 + if (IS_WEB) { 145 if (!headerRef.current) return 146 const measurement = (headerRef.current as Element).getBoundingClientRect() 147 setHeaderHeight(measurement.height) ··· 150 151 useFocusEffect( 152 useNonReactiveCallback(() => { 153 + if (IS_WEB) { 154 setSearchText(queryParam) 155 } 156 }), ··· 173 setShowAutocomplete(false) 174 updateSearchHistory(item) 175 176 + if (IS_WEB) { 177 // @ts-expect-error route is not typesafe 178 navigation.push(route.name, {...route.params, q: item}) 179 } else { ··· 188 scrollToTopWeb() 189 textInput.current?.blur() 190 setShowAutocomplete(false) 191 + if (IS_WEB) { 192 // Empty params resets the URL to be /search rather than /search?q= 193 // Also clear the tab parameter 194 const { ··· 211 }, [navigateToItem, searchText]) 212 213 const onAutocompleteResultPress = useCallback(() => { 214 + if (IS_WEB) { 215 setShowAutocomplete(false) 216 } else { 217 textInput.current?.blur() ··· 238 ) 239 240 const onSoftReset = useCallback(() => { 241 + if (IS_WEB) { 242 // Empty params resets the URL to be /search rather than /search?q= 243 // Also clear the tab parameter when soft resetting 244 const { ··· 265 ) 266 267 const onSearchInputFocus = useCallback(() => { 268 + if (IS_WEB) { 269 // Prevent a jump on iPad by ensuring that 270 // the initial focused render has no result list. 271 requestAnimationFrame(() => { ··· 282 283 // If a tab is specified, set the tab parameter 284 if (tab) { 285 + if (IS_WEB) { 286 navigation.setParams({...route.params, tab}) 287 } else { 288 navigation.setParams({tab}) ··· 299 <View 300 ref={headerRef} 301 onLayout={evt => { 302 + if (IS_WEB) setHeaderHeight(evt.nativeEvent.layout.height) 303 }} 304 style={[ 305 a.relative, ··· 581 } 582 583 function scrollToTopWeb() { 584 + if (IS_WEB) { 585 window.scrollTo(0, 0) 586 } 587 }
+2 -2
src/screens/Search/components/AutocompleteResults.tsx
··· 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 - import {isNative} from '#/platform/detection' 8 import {useModerationOpts} from '#/state/preferences/moderation-opts' 9 import {SearchLinkCard} from '#/view/shell/desktop/Search' 10 import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard' 11 import {atoms as a, native} from '#/alf' 12 import * as Layout from '#/components/Layout' 13 14 let AutocompleteResults = ({ 15 isAutocompleteFetching, ··· 45 label={_(msg`Search for "${searchText}"`)} 46 onPress={native(onSubmit)} 47 to={ 48 - isNative 49 ? undefined 50 : `/search?q=${encodeURIComponent(searchText)}` 51 }
··· 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 import {useModerationOpts} from '#/state/preferences/moderation-opts' 8 import {SearchLinkCard} from '#/view/shell/desktop/Search' 9 import {SearchProfileCard} from '#/screens/Search/components/SearchProfileCard' 10 import {atoms as a, native} from '#/alf' 11 import * as Layout from '#/components/Layout' 12 + import {IS_NATIVE} from '#/env' 13 14 let AutocompleteResults = ({ 15 isAutocompleteFetching, ··· 45 label={_(msg`Search for "${searchText}"`)} 46 onPress={native(onSubmit)} 47 to={ 48 + IS_NATIVE 49 ? undefined 50 : `/search?q=${encodeURIComponent(searchText)}` 51 }
+2 -2
src/screens/Search/modules/ExploreRecommendations.tsx
··· 3 import {Trans} from '@lingui/macro' 4 5 import {logger} from '#/logger' 6 - import {isWeb} from '#/platform/detection' 7 import { 8 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, 9 useTrendingTopics, ··· 17 TrendingTopicSkeleton, 18 } from '#/components/TrendingTopics' 19 import {Text} from '#/components/Typography' 20 21 // 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,
··· 3 import {Trans} from '@lingui/macro' 4 5 import {logger} from '#/logger' 6 import { 7 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, 8 useTrendingTopics, ··· 16 TrendingTopicSkeleton, 17 } from '#/components/TrendingTopics' 18 import {Text} from '#/components/Typography' 19 + import {IS_WEB} from '#/env' 20 21 // 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
··· 11 12 import {STATUS_PAGE_URL} from '#/lib/constants' 13 import {type CommonNavigatorParams} from '#/lib/routes/types' 14 - import {isAndroid, isIOS, isNative} from '#/platform/detection' 15 import * as Toast from '#/view/com/util/Toast' 16 import * as SettingsList from '#/screens/Settings/components/SettingsList' 17 import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' ··· 22 import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' 23 import * as Layout from '#/components/Layout' 24 import {Loader} from '#/components/Loader' 25 import * as env from '#/env' 26 import {useDemoMode} from '#/storage/hooks/demo-mode' 27 import {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
··· 11 12 import {STATUS_PAGE_URL} from '#/lib/constants' 13 import {type CommonNavigatorParams} from '#/lib/routes/types' 14 import * as Toast from '#/view/com/util/Toast' 15 import * as SettingsList from '#/screens/Settings/components/SettingsList' 16 import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' ··· 21 import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench' 22 import * as Layout from '#/components/Layout' 23 import {Loader} from '#/components/Loader' 24 + import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env' 25 import * as env from '#/env' 26 import {useDemoMode} from '#/storage/hooks/demo-mode' 27 import {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
··· 3 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 4 5 import {type CommonNavigatorParams} from '#/lib/routes/types' 6 - import {isNative} from '#/platform/detection' 7 import { 8 useHapticsDisabled, 9 useRequireAltTextEnabled, ··· 20 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 21 import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' 22 import * as Layout from '#/components/Layout' 23 24 type 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]}>
··· 3 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 4 5 import {type CommonNavigatorParams} from '#/lib/routes/types' 6 import { 7 useHapticsDisabled, 8 useRequireAltTextEnabled, ··· 19 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 20 import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' 21 import * as Layout from '#/components/Layout' 22 + import {IS_NATIVE} from '#/env' 23 24 type 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
··· 8 import {PressableScale} from '#/lib/custom-animations/PressableScale' 9 import {type CommonNavigatorParams} from '#/lib/routes/types' 10 import {useGate} from '#/lib/statsig/statsig' 11 - import {isAndroid} from '#/platform/detection' 12 import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' 13 import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types' 14 import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets' ··· 16 import * as Toggle from '#/components/forms/Toggle' 17 import * as Layout from '#/components/Layout' 18 import {Text} from '#/components/Typography' 19 import {IS_INTERNAL} from '#/env' 20 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'> ··· 29 ) 30 31 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`),
··· 8 import {PressableScale} from '#/lib/custom-animations/PressableScale' 9 import {type CommonNavigatorParams} from '#/lib/routes/types' 10 import {useGate} from '#/lib/statsig/statsig' 11 import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage' 12 import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types' 13 import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets' ··· 15 import * as Toggle from '#/components/forms/Toggle' 16 import * as Layout from '#/components/Layout' 17 import {Text} from '#/components/Typography' 18 + import {IS_ANDROID} from '#/env' 19 import {IS_INTERNAL} from '#/env' 20 21 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'> ··· 29 ) 30 31 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' 16 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 17 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 18 import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' ··· 24 import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 25 import * as Layout from '#/components/Layout' 26 import {Text} from '#/components/Typography' 27 import {IS_INTERNAL} from '#/env' 28 import * as SettingsList from './components/SettingsList' 29 ··· 165 onChange={onChangeFontScale} 166 /> 167 168 - {isNative && IS_INTERNAL && ( 169 <> 170 <SettingsList.Divider /> 171 <AppIconSettingsListItem />
··· 12 type CommonNavigatorParams, 13 type NativeStackScreenProps, 14 } from '#/lib/routes/types' 15 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 16 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 17 import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' ··· 23 import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase' 24 import * as Layout from '#/components/Layout' 25 import {Text} from '#/components/Typography' 26 + import {IS_NATIVE} from '#/env' 27 import {IS_INTERNAL} from '#/env' 28 import * as SettingsList from './components/SettingsList' 29 ··· 165 onChange={onChangeFontScale} 166 /> 167 168 + {IS_NATIVE && IS_INTERNAL && ( 169 <> 170 <SettingsList.Divider /> 171 <AppIconSettingsListItem />
+2 -2
src/screens/Settings/ContentAndMediaSettings.tsx
··· 4 5 import {type CommonNavigatorParams} from '#/lib/routes/types' 6 import {logEvent} from '#/lib/statsig/statsig' 7 - import {isNative} from '#/platform/detection' 8 import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' 9 import { 10 useInAppBrowser, ··· 26 import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' 27 import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' 28 import * as Layout from '#/components/Layout' 29 import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle' 30 31 type 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`)}
··· 4 5 import {type CommonNavigatorParams} from '#/lib/routes/types' 6 import {logEvent} from '#/lib/statsig/statsig' 7 import {useAutoplayDisabled, useSetAutoplayDisabled} from '#/state/preferences' 8 import { 9 useInAppBrowser, ··· 25 import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' 26 import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' 27 import * as Layout from '#/components/Layout' 28 + import {IS_NATIVE} from '#/env' 29 import {LiveEventFeedsSettingsToggle} from '#/features/liveEvents/components/LiveEventFeedsSettingsToggle' 30 31 type 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' 21 import {cleanError, isNetworkError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 - import {isNative} from '#/platform/detection' 24 import { 25 updateProfileShadow, 26 useProfileShadow, ··· 48 import * as ProfileCard from '#/components/ProfileCard' 49 import * as Toast from '#/components/Toast' 50 import {Text} from '#/components/Typography' 51 import type * as bsky from '#/types/bsky' 52 import {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' 21 import {cleanError, isNetworkError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 import { 24 updateProfileShadow, 25 useProfileShadow, ··· 47 import * as ProfileCard from '#/components/ProfileCard' 48 import * as Toast from '#/components/Toast' 49 import {Text} from '#/components/Typography' 50 + import {IS_NATIVE} from '#/env' 51 import type * as bsky from '#/types/bsky' 52 import {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 />
+5 -5
src/screens/Settings/NotificationSettings/index.tsx
··· 11 type AllNavigatorParams, 12 type NativeStackScreenProps, 13 } from '#/lib/routes/types' 14 - import {isAndroid, isIOS, isWeb} from '#/platform/detection' 15 import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 16 import {atoms as a} from '#/alf' 17 import {Admonition} from '#/components/Admonition' ··· 31 } from '#/components/icons/Repost' 32 import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' 33 import * as Layout from '#/components/Layout' 34 import * as SettingsList from '../components/SettingsList' 35 import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 36 ··· 45 const {data: permissions, refetch} = useQuery({ 46 queryKey: RQKEY, 47 queryFn: async () => { 48 - if (isWeb) return null 49 return await Notification.getPermissionsAsync() 50 }, 51 }) ··· 58 }, [appState, refetch]) 59 60 const onRequestPermissions = async () => { 61 - if (isWeb) return 62 if (permissions?.canAskAgain) { 63 const response = await Notification.requestPermissionsAsync() 64 queryClient.setQueryData(RQKEY, response) 65 } else { 66 - if (isAndroid) { 67 try { 68 await Linking.sendIntent( 69 'android.settings.APP_NOTIFICATION_SETTINGS', ··· 77 } catch { 78 Linking.openSettings() 79 } 80 - } else if (isIOS) { 81 Linking.openSettings() 82 } 83 }
··· 11 type AllNavigatorParams, 12 type NativeStackScreenProps, 13 } from '#/lib/routes/types' 14 import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' 15 import {atoms as a} from '#/alf' 16 import {Admonition} from '#/components/Admonition' ··· 30 } from '#/components/icons/Repost' 31 import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes' 32 import * as Layout from '#/components/Layout' 33 + import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' 34 import * as SettingsList from '../components/SettingsList' 35 import {ItemTextWithSubtitle} from './components/ItemTextWithSubtitle' 36 ··· 45 const {data: permissions, refetch} = useQuery({ 46 queryKey: RQKEY, 47 queryFn: async () => { 48 + if (IS_WEB) return null 49 return await Notification.getPermissionsAsync() 50 }, 51 }) ··· 58 }, [appState, refetch]) 59 60 const onRequestPermissions = async () => { 61 + if (IS_WEB) return 62 if (permissions?.canAskAgain) { 63 const response = await Notification.requestPermissionsAsync() 64 queryClient.setQueryData(RQKEY, response) 65 } else { 66 + if (IS_ANDROID) { 67 try { 68 await Linking.sendIntent( 69 'android.settings.APP_NOTIFICATION_SETTINGS', ··· 77 } catch { 78 Linking.openSettings() 79 } 80 + } else if (IS_IOS) { 81 Linking.openSettings() 82 } 83 }
+4 -4
src/screens/Settings/Settings.tsx
··· 19 import {useGate} from '#/lib/statsig/statsig' 20 import {sanitizeDisplayName} from '#/lib/strings/display-names' 21 import {sanitizeHandle} from '#/lib/strings/handles' 22 - import {isIOS, isNative} from '#/platform/detection' 23 import {useProfileShadow} from '#/state/cache/profile-shadow' 24 import * as persisted from '#/state/persisted' 25 import {clearStorage} from '#/state/persisted' ··· 71 shouldShowVerificationCheckButton, 72 VerificationCheckButton, 73 } from '#/components/verification/VerificationCheckButton' 74 import {IS_INTERNAL} from '#/env' 75 import {device, useStorage} from '#/storage' 76 import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' ··· 213 <Trans>Content and media</Trans> 214 </SettingsList.ItemText> 215 </SettingsList.LinkItem> 216 - {isNative && 217 findContactsEnabled && 218 !gate('disable_settings_find_contacts') && ( 219 <SettingsList.LinkItem ··· 510 <Trans>Clear all storage data (restart after this)</Trans> 511 </SettingsList.ItemText> 512 </SettingsList.PressableItem> 513 - {isIOS ? ( 514 <SettingsList.PressableItem 515 onPress={onPressApplyOta} 516 label={_(msg`Apply Pull Request`)}> ··· 519 </SettingsList.ItemText> 520 </SettingsList.PressableItem> 521 ) : null} 522 - {isNative && isCurrentlyRunningPullRequestDeployment ? ( 523 <SettingsList.PressableItem 524 onPress={revertToEmbedded} 525 label={_(msg`Unapply Pull Request`)}>
··· 19 import {useGate} from '#/lib/statsig/statsig' 20 import {sanitizeDisplayName} from '#/lib/strings/display-names' 21 import {sanitizeHandle} from '#/lib/strings/handles' 22 import {useProfileShadow} from '#/state/cache/profile-shadow' 23 import * as persisted from '#/state/persisted' 24 import {clearStorage} from '#/state/persisted' ··· 70 shouldShowVerificationCheckButton, 71 VerificationCheckButton, 72 } from '#/components/verification/VerificationCheckButton' 73 + import {IS_IOS, IS_NATIVE} from '#/env' 74 import {IS_INTERNAL} from '#/env' 75 import {device, useStorage} from '#/storage' 76 import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged' ··· 213 <Trans>Content and media</Trans> 214 </SettingsList.ItemText> 215 </SettingsList.LinkItem> 216 + {IS_NATIVE && 217 findContactsEnabled && 218 !gate('disable_settings_find_contacts') && ( 219 <SettingsList.LinkItem ··· 510 <Trans>Clear all storage data (restart after this)</Trans> 511 </SettingsList.ItemText> 512 </SettingsList.PressableItem> 513 + {IS_IOS ? ( 514 <SettingsList.PressableItem 515 onPress={onPressApplyOta} 516 label={_(msg`Apply Pull Request`)}> ··· 519 </SettingsList.ItemText> 520 </SettingsList.PressableItem> 521 ) : null} 522 + {IS_NATIVE && isCurrentlyRunningPullRequestDeployment ? ( 523 <SettingsList.PressableItem 524 onPress={revertToEmbedded} 525 label={_(msg`Unapply Pull Request`)}>
+2 -2
src/screens/Settings/components/AddAppPasswordDialog.tsx
··· 13 import {useLingui} from '@lingui/react' 14 import {useMutation} from '@tanstack/react-query' 15 16 - import {isWeb} from '#/platform/detection' 17 import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords' 18 import {atoms as a, native, useTheme} from '#/alf' 19 import {Admonition} from '#/components/Admonition' ··· 24 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 25 import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' 26 import {Text} from '#/components/Typography' 27 import {CopyButton} from './CopyButton' 28 29 export 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>
··· 13 import {useLingui} from '@lingui/react' 14 import {useMutation} from '@tanstack/react-query' 15 16 import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords' 17 import {atoms as a, native, useTheme} from '#/alf' 18 import {Admonition} from '#/components/Admonition' ··· 23 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' 24 import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' 25 import {Text} from '#/components/Typography' 26 + import {IS_WEB} from '#/env' 27 import {CopyButton} from './CopyButton' 28 29 export 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>
+2 -2
src/screens/Settings/components/ChangePasswordDialog.tsx
··· 7 import {cleanError, isNetworkError} from '#/lib/strings/errors' 8 import {checkAndFormatResetCode} from '#/lib/strings/password' 9 import {logger} from '#/logger' 10 - import {isNative} from '#/platform/detection' 11 import {useAgent, useSession} from '#/state/session' 12 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 13 import {android, atoms as a, web} from '#/alf' ··· 16 import * as TextField from '#/components/forms/TextField' 17 import {Loader} from '#/components/Loader' 18 import {Text} from '#/components/Typography' 19 20 enum 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"
··· 7 import {cleanError, isNetworkError} from '#/lib/strings/errors' 8 import {checkAndFormatResetCode} from '#/lib/strings/password' 9 import {logger} from '#/logger' 10 import {useAgent, useSession} from '#/state/session' 11 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 12 import {android, atoms as a, web} from '#/alf' ··· 15 import * as TextField from '#/components/forms/TextField' 16 import {Loader} from '#/components/Loader' 17 import {Text} from '#/components/Typography' 18 + import {IS_NATIVE} from '#/env' 19 20 enum 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"
+2 -2
src/screens/Settings/components/DisableEmail2FADialog.tsx
··· 4 import {useLingui} from '@lingui/react' 5 6 import {cleanError} from '#/lib/strings/errors' 7 - import {isNative} from '#/platform/detection' 8 import {useAgent, useSession} from '#/state/session' 9 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 10 import * as Toast from '#/view/com/util/Toast' ··· 15 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 16 import {Loader} from '#/components/Loader' 17 import {P, Text} from '#/components/Typography' 18 19 enum Stages { 20 Email, ··· 193 </View> 194 ) : undefined} 195 196 - {!gtMobile && isNative && <View style={{height: 40}} />} 197 </View> 198 </Dialog.ScrollableInner> 199 </Dialog.Outer>
··· 4 import {useLingui} from '@lingui/react' 5 6 import {cleanError} from '#/lib/strings/errors' 7 import {useAgent, useSession} from '#/state/session' 8 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 9 import * as Toast from '#/view/com/util/Toast' ··· 14 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 15 import {Loader} from '#/components/Loader' 16 import {P, Text} from '#/components/Typography' 17 + import {IS_NATIVE} from '#/env' 18 19 enum Stages { 20 Email, ··· 193 </View> 194 ) : undefined} 195 196 + {!gtMobile && IS_NATIVE && <View style={{height: 40}} />} 197 </View> 198 </Dialog.ScrollableInner> 199 </Dialog.Outer>
+8 -6
src/screens/Signup/StepCaptcha/index.tsx
··· 7 8 import {createFullHandle} from '#/lib/strings/handles' 9 import {logger} from '#/logger' 10 - import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 11 import {useSignupContext} from '#/screens/Signup/state' 12 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 13 import {atoms as a, useTheme} from '#/alf' 14 import {FormError} from '#/components/forms/FormError' 15 import {GCP_PROJECT_ID} from '#/env' 16 import {BackNextButtons} from '../BackNextButtons' 17 18 const CAPTCHA_PATH = 19 - isWeb || GCP_PROJECT_ID === 0 ? '/gate/signup' : '/gate/signup/attempt-attest' 20 21 export 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) 87 88 - 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 }
··· 7 8 import {createFullHandle} from '#/lib/strings/handles' 9 import {logger} from '#/logger' 10 import {useSignupContext} from '#/screens/Signup/state' 11 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 12 import {atoms as a, useTheme} from '#/alf' 13 import {FormError} from '#/components/forms/FormError' 14 + import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 15 import {GCP_PROJECT_ID} from '#/env' 16 import {BackNextButtons} from '../BackNextButtons' 17 18 const CAPTCHA_PATH = 19 + IS_WEB || GCP_PROJECT_ID === 0 20 + ? '/gate/signup' 21 + : '/gate/signup/attempt-attest' 22 23 export 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) 89 90 + 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
··· 7 8 import {isEmailMaybeInvalid} from '#/lib/strings/email' 9 import {logger} from '#/logger' 10 - import {isNative} from '#/platform/detection' 11 import {useSignupContext} from '#/screens/Signup/state' 12 import {Policies} from '#/screens/Signup/StepInfo/Policies' 13 import {atoms as a, native} from '#/alf' ··· 31 MIN_ACCESS_AGE, 32 useAgeAssuranceRegionConfigWithFallback, 33 } from '#/ageAssurance/util' 34 import { 35 useDeviceGeolocationApi, 36 useIsDeviceGeolocationGranted, ··· 325 </Trans> 326 )} 327 </Admonition.Text> 328 - {isNative && 329 !isDeviceGeolocationGranted && 330 isOverAppMinAccessAge && ( 331 <Admonition.Text> ··· 357 ) : undefined} 358 </View> 359 360 - {isNative && ( 361 <DeviceLocationRequestDialog 362 control={locationControl} 363 onLocationAcquired={props => {
··· 7 8 import {isEmailMaybeInvalid} from '#/lib/strings/email' 9 import {logger} from '#/logger' 10 import {useSignupContext} from '#/screens/Signup/state' 11 import {Policies} from '#/screens/Signup/StepInfo/Policies' 12 import {atoms as a, native} from '#/alf' ··· 30 MIN_ACCESS_AGE, 31 useAgeAssuranceRegionConfigWithFallback, 32 } from '#/ageAssurance/util' 33 + import {IS_NATIVE} from '#/env' 34 import { 35 useDeviceGeolocationApi, 36 useIsDeviceGeolocationGranted, ··· 325 </Trans> 326 )} 327 </Admonition.Text> 328 + {IS_NATIVE && 329 !isDeviceGeolocationGranted && 330 isOverAppMinAccessAge && ( 331 <Admonition.Text> ··· 357 ) : undefined} 358 </View> 359 360 + {IS_NATIVE && ( 361 <DeviceLocationRequestDialog 362 control={locationControl} 363 onLocationAcquired={props => {
+2 -2
src/screens/Signup/index.tsx
··· 8 9 import {FEEDBACK_FORM_URL} from '#/lib/constants' 10 import {logger} from '#/logger' 11 - import {isAndroid} from '#/platform/detection' 12 import {useServiceQuery} from '#/state/queries/service' 13 import {useStarterPackQuery} from '#/state/queries/starter-packs' 14 import {useActiveStarterPack} from '#/state/shell/starter-pack' ··· 30 import {InlineLinkText} from '#/components/Link' 31 import {ScreenTransition} from '#/components/ScreenTransition' 32 import {Text} from '#/components/Typography' 33 import {GCP_PROJECT_ID} from '#/env' 34 import * as bsky from '#/types/bsky' 35 ··· 108 109 // 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 =>
··· 8 9 import {FEEDBACK_FORM_URL} from '#/lib/constants' 10 import {logger} from '#/logger' 11 import {useServiceQuery} from '#/state/queries/service' 12 import {useStarterPackQuery} from '#/state/queries/starter-packs' 13 import {useActiveStarterPack} from '#/state/shell/starter-pack' ··· 29 import {InlineLinkText} from '#/components/Link' 30 import {ScreenTransition} from '#/components/ScreenTransition' 31 import {Text} from '#/components/Typography' 32 + import {IS_ANDROID} from '#/env' 33 import {GCP_PROJECT_ID} from '#/env' 34 import * as bsky from '#/types/bsky' 35 ··· 108 109 // 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
··· 6 import {useLingui} from '@lingui/react' 7 8 import {logger} from '#/logger' 9 - import {isIOS, isWeb} from '#/platform/detection' 10 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 11 import {useOnboardingDispatch} from '#/state/shell' 12 import {Logo} from '#/view/icons/Logo' ··· 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import {Loader} from '#/components/Loader' 16 import {P, Text} from '#/components/Typography' 17 18 const COL_WIDTH = 400 19 ··· 98 </Button> 99 ) 100 101 - const webLayout = isWeb && gtMobile 102 103 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}}
··· 6 import {useLingui} from '@lingui/react' 7 8 import {logger} from '#/logger' 9 import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' 10 import {useOnboardingDispatch} from '#/state/shell' 11 import {Logo} from '#/view/icons/Logo' ··· 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 import {Loader} from '#/components/Loader' 15 import {P, Text} from '#/components/Typography' 16 + import {IS_IOS, IS_WEB} from '#/env' 17 18 const COL_WIDTH = 400 19 ··· 98 </Button> 99 ) 100 101 + const webLayout = IS_WEB && gtMobile 102 103 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}}
+4 -4
src/screens/StarterPack/StarterPackLandingScreen.tsx
··· 11 import {msg, Trans} from '@lingui/macro' 12 import {useLingui} from '@lingui/react' 13 14 - import {isAndroidWeb} from '#/lib/browser' 15 import {JOINED_THIS_WEEK} from '#/lib/constants' 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 import {logEvent} from '#/lib/statsig/statsig' 18 import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack' 19 - import {isWeb} from '#/platform/detection' 20 import {useModerationOpts} from '#/state/preferences/moderation-opts' 21 import {useStarterPackQuery} from '#/state/queries/starter-packs' 22 import { ··· 38 import * as Prompt from '#/components/Prompt' 39 import {RichText} from '#/components/RichText' 40 import {Text} from '#/components/Typography' 41 import * as bsky from '#/types/bsky' 42 43 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) ··· 142 postAppClipMessage({ 143 action: 'present', 144 }) 145 - } else if (isAndroidWeb) { 146 androidDialogControl.open() 147 } else { 148 onContinue() ··· 359 /> 360 </Prompt.Actions> 361 </Prompt.Outer> 362 - {isWeb && ( 363 <meta 364 name="apple-itunes-app" 365 content="app-id=xyz.blueskyweb.app, app-clip-bundle-id=xyz.blueskyweb.app.AppClip, app-clip-display=card"
··· 11 import {msg, Trans} from '@lingui/macro' 12 import {useLingui} from '@lingui/react' 13 14 + import {IS_ANDROIDWeb} from '#/lib/browser' 15 import {JOINED_THIS_WEEK} from '#/lib/constants' 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 import {logEvent} from '#/lib/statsig/statsig' 18 import {createStarterPackGooglePlayUri} from '#/lib/strings/starter-pack' 19 import {useModerationOpts} from '#/state/preferences/moderation-opts' 20 import {useStarterPackQuery} from '#/state/queries/starter-packs' 21 import { ··· 37 import * as Prompt from '#/components/Prompt' 38 import {RichText} from '#/components/RichText' 39 import {Text} from '#/components/Typography' 40 + import {IS_WEB} from '#/env' 41 import * as bsky from '#/types/bsky' 42 43 const AnimatedPressable = Animated.createAnimatedComponent(Pressable) ··· 142 postAppClipMessage({ 143 action: 'present', 144 }) 145 + } else if (IS_ANDROIDWeb) { 146 androidDialogControl.open() 147 } else { 148 onContinue() ··· 359 /> 360 </Prompt.Actions> 361 </Prompt.Outer> 362 + {IS_WEB && ( 363 <meta 364 name="apple-itunes-app" 365 content="app-id=xyz.blueskyweb.app, app-clip-bundle-id=xyz.blueskyweb.app.AppClip, app-clip-display=card"
+4 -4
src/screens/StarterPack/StarterPackScreen.tsx
··· 27 import {cleanError} from '#/lib/strings/errors' 28 import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 29 import {logger} from '#/logger' 30 - import {isWeb} from '#/platform/detection' 31 import {updateProfileShadow} from '#/state/cache/profile-shadow' 32 import {useModerationOpts} from '#/state/preferences/moderation-opts' 33 import {getAllListMembers} from '#/state/queries/list-members' ··· 72 import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog' 73 import {ShareDialog} from '#/components/StarterPack/ShareDialog' 74 import {Text} from '#/components/Typography' 75 import * as bsky from '#/types/bsky' 76 77 type StarterPackScreeProps = NativeStackScreenProps< ··· 608 <Menu.Group> 609 <Menu.Item 610 label={ 611 - isWeb 612 ? _(msg`Copy link to starter pack`) 613 : _(msg`Share via...`) 614 } 615 testID="shareStarterPackLinkBtn" 616 onPress={onOpenShareDialog}> 617 <Menu.ItemText> 618 - {isWeb ? ( 619 <Trans>Copy link</Trans> 620 ) : ( 621 <Trans>Share via...</Trans> 622 )} 623 </Menu.ItemText> 624 <Menu.ItemIcon 625 - icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} 626 position="right" 627 /> 628 </Menu.Item>
··· 27 import {cleanError} from '#/lib/strings/errors' 28 import {getStarterPackOgCard} from '#/lib/strings/starter-pack' 29 import {logger} from '#/logger' 30 import {updateProfileShadow} from '#/state/cache/profile-shadow' 31 import {useModerationOpts} from '#/state/preferences/moderation-opts' 32 import {getAllListMembers} from '#/state/queries/list-members' ··· 71 import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog' 72 import {ShareDialog} from '#/components/StarterPack/ShareDialog' 73 import {Text} from '#/components/Typography' 74 + import {IS_WEB} from '#/env' 75 import * as bsky from '#/types/bsky' 76 77 type StarterPackScreeProps = NativeStackScreenProps< ··· 608 <Menu.Group> 609 <Menu.Item 610 label={ 611 + IS_WEB 612 ? _(msg`Copy link to starter pack`) 613 : _(msg`Share via...`) 614 } 615 testID="shareStarterPackLinkBtn" 616 onPress={onOpenShareDialog}> 617 <Menu.ItemText> 618 + {IS_WEB ? ( 619 <Trans>Copy link</Trans> 620 ) : ( 621 <Trans>Share via...</Trans> 622 )} 623 </Menu.ItemText> 624 <Menu.ItemIcon 625 + icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon} 626 position="right" 627 /> 628 </Menu.Item>
+2 -2
src/screens/StarterPack/Wizard/StepProfiles.tsx
··· 4 import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' 5 import {Trans} from '@lingui/macro' 6 7 - import {isNative} from '#/platform/detection' 8 import {useA11y} from '#/state/a11y' 9 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 10 import {useActorSearch} from '#/state/queries/actor-search' ··· 16 import {ScreenTransition} from '#/components/ScreenTransition' 17 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' 18 import {Text} from '#/components/Typography' 19 import type * as bsky from '#/types/bsky' 20 21 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { ··· 89 onEndReached={ 90 !query && !screenReaderEnabled ? () => fetchNextPage() : undefined 91 } 92 - onEndReachedThreshold={isNative ? 2 : 0.25} 93 keyboardDismissMode="on-drag" 94 ListEmptyComponent={ 95 <View style={[a.flex_1, a.align_center, a.mt_lg, a.px_lg]}>
··· 4 import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' 5 import {Trans} from '@lingui/macro' 6 7 import {useA11y} from '#/state/a11y' 8 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' 9 import {useActorSearch} from '#/state/queries/actor-search' ··· 15 import {ScreenTransition} from '#/components/ScreenTransition' 16 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' 17 import {Text} from '#/components/Typography' 18 + import {IS_NATIVE} from '#/env' 19 import type * as bsky from '#/types/bsky' 20 21 function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { ··· 89 onEndReached={ 90 !query && !screenReaderEnabled ? () => fetchNextPage() : undefined 91 } 92 + onEndReachedThreshold={IS_NATIVE ? 2 : 0.25} 93 keyboardDismissMode="on-drag" 94 ListEmptyComponent={ 95 <View style={[a.flex_1, a.align_center, a.mt_lg, a.px_lg]}>
+3 -3
src/screens/StarterPack/Wizard/index.tsx
··· 31 parseStarterPackUri, 32 } from '#/lib/strings/starter-pack' 33 import {logger} from '#/logger' 34 - import {isNative} from '#/platform/detection' 35 import {useModerationOpts} from '#/state/preferences/moderation-opts' 36 import {useAllListMembersQuery} from '#/state/queries/list-members' 37 import {useProfileQuery} from '#/state/queries/profile' ··· 59 import {Loader} from '#/components/Loader' 60 import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog' 61 import {Text} from '#/components/Typography' 62 import type * as bsky from '#/types/bsky' 63 import {Provider} from './State' 64 ··· 435 { 436 paddingBottom: a.pb_lg.paddingBottom + bottomInset, 437 }, 438 - isNative && [ 439 a.border_l, 440 a.border_r, 441 t.atoms.shadow_md, ··· 601 a.w_full, 602 a.align_center, 603 a.gap_2xl, 604 - isNative ? a.mt_sm : a.mt_md, 605 ]}> 606 {state.currentStep === 'Profiles' && items.length < 8 && ( 607 <Text
··· 31 parseStarterPackUri, 32 } from '#/lib/strings/starter-pack' 33 import {logger} from '#/logger' 34 import {useModerationOpts} from '#/state/preferences/moderation-opts' 35 import {useAllListMembersQuery} from '#/state/queries/list-members' 36 import {useProfileQuery} from '#/state/queries/profile' ··· 58 import {Loader} from '#/components/Loader' 59 import {WizardEditListDialog} from '#/components/StarterPack/Wizard/WizardEditListDialog' 60 import {Text} from '#/components/Typography' 61 + import {IS_NATIVE} from '#/env' 62 import type * as bsky from '#/types/bsky' 63 import {Provider} from './State' 64 ··· 435 { 436 paddingBottom: a.pb_lg.paddingBottom + bottomInset, 437 }, 438 + IS_NATIVE && [ 439 a.border_l, 440 a.border_r, 441 t.atoms.shadow_md, ··· 601 a.w_full, 602 a.align_center, 603 a.gap_2xl, 604 + IS_NATIVE ? a.mt_sm : a.mt_md, 605 ]}> 606 {state.currentStep === 'Profiles' && items.length < 8 && ( 607 <Text
+2 -2
src/screens/Takendown.tsx
··· 14 } from '#/lib/constants' 15 import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 16 import {cleanError} from '#/lib/strings/errors' 17 - import {isWeb} from '#/platform/detection' 18 import {useAgent, useSession, useSessionApi} from '#/state/session' 19 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 20 import {Logo} from '#/view/icons/Logo' ··· 24 import {SimpleInlineLinkText} from '#/components/Link' 25 import {Loader} from '#/components/Loader' 26 import {P, Text} from '#/components/Typography' 27 28 const COL_WIDTH = 400 29 ··· 119 </Button> 120 ) 121 122 - const webLayout = isWeb && gtMobile 123 124 useEnableKeyboardController(true) 125
··· 14 } from '#/lib/constants' 15 import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 16 import {cleanError} from '#/lib/strings/errors' 17 import {useAgent, useSession, useSessionApi} from '#/state/session' 18 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 19 import {Logo} from '#/view/icons/Logo' ··· 23 import {SimpleInlineLinkText} from '#/components/Link' 24 import {Loader} from '#/components/Loader' 25 import {P, Text} from '#/components/Typography' 26 + import {IS_WEB} from '#/env' 27 28 const COL_WIDTH = 400 29 ··· 119 </Button> 120 ) 121 122 + const webLayout = IS_WEB && gtMobile 123 124 useEnableKeyboardController(true) 125
+4 -4
src/screens/VideoFeed/index.tsx
··· 57 import {cleanError} from '#/lib/strings/errors' 58 import {sanitizeHandle} from '#/lib/strings/handles' 59 import {logger} from '#/logger' 60 - import {isAndroid} from '#/platform/detection' 61 import {useA11y} from '#/state/a11y' 62 import { 63 POST_TOMBSTONE, ··· 101 import {PostControls} from '#/components/PostControls' 102 import {RichText} from '#/components/RichText' 103 import {Text} from '#/components/Typography' 104 import * as bsky from '#/types/bsky' 105 import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber' 106 ··· 590 embed: AppBskyEmbedVideo.View 591 }) { 592 const {bottom} = useSafeAreaInsets() 593 - const [isReady, setIsReady] = useState(!isAndroid) 594 595 useEventListener(player, 'timeUpdate', evt => { 596 - if (isAndroid && !isReady && evt.currentTime >= 0.05) { 597 setIsReady(true) 598 } 599 }) ··· 920 </LinearGradient> 921 </View> 922 {/* 923 - {isAndroid && status === 'loading' && ( 924 <View 925 style={[ 926 a.absolute,
··· 57 import {cleanError} from '#/lib/strings/errors' 58 import {sanitizeHandle} from '#/lib/strings/handles' 59 import {logger} from '#/logger' 60 import {useA11y} from '#/state/a11y' 61 import { 62 POST_TOMBSTONE, ··· 100 import {PostControls} from '#/components/PostControls' 101 import {RichText} from '#/components/RichText' 102 import {Text} from '#/components/Typography' 103 + import {IS_ANDROID} from '#/env' 104 import * as bsky from '#/types/bsky' 105 import {Scrubber, VIDEO_PLAYER_BOTTOM_INSET} from './components/Scrubber' 106 ··· 590 embed: AppBskyEmbedVideo.View 591 }) { 592 const {bottom} = useSafeAreaInsets() 593 + const [isReady, setIsReady] = useState(!IS_ANDROID) 594 595 useEventListener(player, 'timeUpdate', evt => { 596 + if (IS_ANDROID && !isReady && evt.currentTime >= 0.05) { 597 setIsReady(true) 598 } 599 }) ··· 920 </LinearGradient> 921 </View> 922 {/* 923 + {IS_ANDROID && status === 'loading' && ( 924 <View 925 style={[ 926 a.absolute,
+2 -2
src/state/a11y.tsx
··· 1 import React from 'react' 2 import {AccessibilityInfo} from 'react-native' 3 4 - import {isWeb} from '#/platform/detection' 5 import {PlatformInfo} from '../../modules/expo-bluesky-swiss-army' 6 7 const Context = React.createContext({ ··· 58 * 59 * @see https://github.com/necolas/react-native-web/discussions/2072 60 */ 61 - screenReaderEnabled: isWeb ? false : screenReaderEnabled, 62 } 63 }, [reduceMotionEnabled, screenReaderEnabled]) 64
··· 1 import React from 'react' 2 import {AccessibilityInfo} from 'react-native' 3 4 + import {IS_WEB} from '#/env' 5 import {PlatformInfo} from '../../modules/expo-bluesky-swiss-army' 6 7 const Context = React.createContext({ ··· 58 * 59 * @see https://github.com/necolas/react-native-web/discussions/2072 60 */ 61 + screenReaderEnabled: IS_WEB ? false : screenReaderEnabled, 62 } 63 }, [reduceMotionEnabled, screenReaderEnabled]) 64
+2 -2
src/state/dialogs/index.tsx
··· 1 import React from 'react' 2 3 - import {isWeb} from '#/platform/detection' 4 import {type DialogControlRefProps} from '#/components/Dialog' 5 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' 6 import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet' 7 8 interface IDialogContext { ··· 62 const openDialogs = React.useRef<Set<string>>(new Set()) 63 64 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()
··· 1 import React from 'react' 2 3 import {type DialogControlRefProps} from '#/components/Dialog' 4 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' 5 + import {IS_WEB} from '#/env' 6 import {BottomSheetNativeComponent} from '../../../modules/bottom-sheet' 7 8 interface IDialogContext { ··· 62 const openDialogs = React.useRef<Set<string>>(new Set()) 63 64 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
··· 18 import {type PickerImage} from '#/lib/media/picker.shared' 19 import {getDataUriSize} from '#/lib/media/util' 20 import {isCancelledError} from '#/lib/strings/errors' 21 - import {isNative} from '#/platform/detection' 22 23 export type ImageTransformation = { 24 crop?: ActionCrop['crop'] ··· 55 let _imageCacheDirectory: string 56 57 function getImageCacheDirectory(): string | null { 58 - if (isNative) { 59 return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer')) 60 } 61 ··· 120 } 121 122 export async function cropImage(img: ComposerImage): Promise<ComposerImage> { 123 - if (!isNative) { 124 return img 125 } 126 ··· 244 } 245 246 async function moveIfNecessary(from: string) { 247 - const cacheDir = isNative && getImageCacheDirectory() 248 249 if (cacheDir && from.startsWith(cacheDir)) { 250 const to = joinPath(cacheDir, nanoid(36)) ··· 260 261 /** Purge files that were created to accomodate image manipulation */ 262 export async function purgeTemporaryImageFiles() { 263 - const cacheDir = isNative && getImageCacheDirectory() 264 265 if (cacheDir) { 266 await deleteAsync(cacheDir, {idempotent: true})
··· 18 import {type PickerImage} from '#/lib/media/picker.shared' 19 import {getDataUriSize} from '#/lib/media/util' 20 import {isCancelledError} from '#/lib/strings/errors' 21 + import {IS_NATIVE} from '#/env' 22 23 export type ImageTransformation = { 24 crop?: ActionCrop['crop'] ··· 55 let _imageCacheDirectory: string 56 57 function getImageCacheDirectory(): string | null { 58 + if (IS_NATIVE) { 59 return (_imageCacheDirectory ??= joinPath(cacheDirectory!, 'bsky-composer')) 60 } 61 ··· 120 } 121 122 export async function cropImage(img: ComposerImage): Promise<ComposerImage> { 123 + if (!IS_NATIVE) { 124 return img 125 } 126 ··· 244 } 245 246 async function moveIfNecessary(from: string) { 247 + const cacheDir = IS_NATIVE && getImageCacheDirectory() 248 249 if (cacheDir && from.startsWith(cacheDir)) { 250 const to = joinPath(cacheDir, nanoid(36)) ··· 260 261 /** Purge files that were created to accomodate image manipulation */ 262 export async function purgeTemporaryImageFiles() { 263 + const cacheDir = IS_NATIVE && getImageCacheDirectory() 264 265 if (cacheDir) { 266 await deleteAsync(cacheDir, {idempotent: true})
+2 -2
src/state/messages/convo/agent.ts
··· 16 isNetworkError, 17 } from '#/lib/strings/errors' 18 import {Logger} from '#/logger' 19 - import {isNative} from '#/platform/detection' 20 import { 21 ACTIVE_POLL_INTERVAL, 22 BACKGROUND_POLL_INTERVAL, ··· 37 } from '#/state/messages/convo/types' 38 import {type MessagesEventBus} from '#/state/messages/events/agent' 39 import {type MessagesEventBusError} from '#/state/messages/events/types' 40 41 const 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' 18 import {Logger} from '#/logger' 19 import { 20 ACTIVE_POLL_INTERVAL, 21 BACKGROUND_POLL_INTERVAL, ··· 36 } from '#/state/messages/convo/types' 37 import {type MessagesEventBus} from '#/state/messages/events/agent' 38 import {type MessagesEventBusError} from '#/state/messages/events/types' 39 + import {IS_NATIVE} from '#/env' 40 41 const 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
··· 1 import React from 'react' 2 3 - import {isWeb} from '#/platform/detection' 4 import * as persisted from '#/state/persisted' 5 6 type 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 32 33 - if (isWeb) { 34 const kawaii = new URLSearchParams(window.location.search).get('kawaii') 35 switch (kawaii) { 36 case 'true':
··· 1 import React from 'react' 2 3 import * as persisted from '#/state/persisted' 4 + import {IS_WEB} from '#/env' 5 6 type 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 32 33 + 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
··· 1 import {useCallback, useMemo, useState} from 'react' 2 import {useQuery, useQueryClient} from '@tanstack/react-query' 3 4 - import {isWeb} from '#/platform/detection' 5 import {useModerationOpts} from '#/state/preferences/moderation-opts' 6 import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences' 7 import { ··· 31 import {useAgent, useSession} from '#/state/session' 32 import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 33 import {useBreakpoints} from '#/alf' 34 35 export * from '#/state/queries/usePostThread/context' 36 export {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])
··· 1 import {useCallback, useMemo, useState} from 'react' 2 import {useQuery, useQueryClient} from '@tanstack/react-query' 3 4 import {useModerationOpts} from '#/state/preferences/moderation-opts' 5 import {useThreadPreferences} from '#/state/queries/preferences/useThreadPreferences' 6 import { ··· 30 import {useAgent, useSession} from '#/state/session' 31 import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 32 import {useBreakpoints} from '#/alf' 33 + import {IS_WEB} from '#/env' 34 35 export * from '#/state/queries/usePostThread/context' 36 export {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
··· 1 import React from 'react' 2 import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' 3 4 - import {isWeb} from '#/platform/detection' 5 import * as persisted from '#/state/persisted' 6 import {useCloseAllActiveElements} from '#/state/util' 7 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 8 import {emitSessionDropped} from '../events' 9 import { 10 agentToSessionAccount, ··· 340 ) 341 342 // @ts-expect-error window type is not declared, debug only 343 - if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent 344 345 const agent = state.currentAgentState.agent as BskyAppAgent 346 const currentAgentRef = React.useRef(agent)
··· 1 import React from 'react' 2 import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' 3 4 import * as persisted from '#/state/persisted' 5 import {useCloseAllActiveElements} from '#/state/util' 6 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 7 + import {IS_WEB} from '#/env' 8 import {emitSessionDropped} from '../events' 9 import { 10 agentToSessionAccount, ··· 340 ) 341 342 // @ts-expect-error window type is not declared, debug only 343 + if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent 344 345 const agent = state.currentAgentState.agent as BskyAppAgent 346 const currentAgentRef = React.useRef(agent)
+2 -2
src/state/shell/logged-out.tsx
··· 1 import React from 'react' 2 3 - import {isWeb} from '#/platform/detection' 4 import {useSession} from '#/state/session' 5 import {useActiveStarterPack} from '#/state/shell/starter-pack' 6 7 type 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,
··· 1 import React from 'react' 2 3 import {useSession} from '#/state/session' 4 import {useActiveStarterPack} from '#/state/shell/starter-pack' 5 + import {IS_WEB} from '#/env' 6 7 type 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
··· 1 import {createContext, useCallback, useContext, useState} from 'react' 2 3 - import {isWeb} from '#/platform/detection' 4 import {type FeedDescriptor} from '#/state/queries/post-feed' 5 import {useSession} from '#/state/session' 6 import {account} from '#/storage' 7 8 type StateContext = FeedDescriptor | null ··· 14 setContext.displayName = 'SelectedFeedSetContext' 15 16 function 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 {}
··· 1 import {createContext, useCallback, useContext, useState} from 'react' 2 3 import {type FeedDescriptor} from '#/state/queries/post-feed' 4 import {useSession} from '#/state/session' 5 + import {IS_WEB} from '#/env' 6 import {account} from '#/storage' 7 8 type StateContext = FeedDescriptor | null ··· 14 setContext.displayName = 'SelectedFeedSetContext' 15 16 function 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 {}
+2 -2
src/view/com/auth/SplashScreen.web.tsx
··· 31 }) => { 32 const {_} = useLingui() 33 const t = useTheme() 34 - const {isTabletOrMobile: isMobileWeb} = useWebMediaQueries() 35 const [showClipOverlay, setShowClipOverlay] = React.useState(false) 36 37 React.useEffect(() => { ··· 78 a.justify_center, 79 // @ts-expect-error web only 80 {paddingBottom: '20vh'}, 81 - isMobileWeb && a.pb_5xl, 82 t.atoms.border_contrast_medium, 83 a.align_center, 84 a.gap_5xl,
··· 31 }) => { 32 const {_} = useLingui() 33 const t = useTheme() 34 + const {isTabletOrMobile: IS_WEB_MOBILE} = useWebMediaQueries() 35 const [showClipOverlay, setShowClipOverlay] = React.useState(false) 36 37 React.useEffect(() => { ··· 78 a.justify_center, 79 // @ts-expect-error web only 80 {paddingBottom: '20vh'}, 81 + IS_WEB_MOBILE && a.pb_5xl, 82 t.atoms.border_contrast_medium, 83 a.align_center, 84 a.gap_5xl,
+19 -19
src/view/com/composer/Composer.tsx
··· 76 import {cleanError} from '#/lib/strings/errors' 77 import {colors} from '#/lib/styles' 78 import {logger} from '#/logger' 79 - import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 80 import {useDialogStateControlContext} from '#/state/dialogs' 81 import {emitPostCreated} from '#/state/events' 82 import { ··· 130 import * as Prompt from '#/components/Prompt' 131 import * as Toast from '#/components/Toast' 132 import {Text as NewText} from '#/components/Typography' 133 import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' 134 import {PostLanguageSelect} from './select-language/PostLanguageSelect' 135 import { ··· 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 }), ··· 366 367 // 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 ) 729 730 - 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> 801 802 <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() 1653 1654 // 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() 1697 1698 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}
··· 76 import {cleanError} from '#/lib/strings/errors' 77 import {colors} from '#/lib/styles' 78 import {logger} from '#/logger' 79 import {useDialogStateControlContext} from '#/state/dialogs' 80 import {emitPostCreated} from '#/state/events' 81 import { ··· 129 import * as Prompt from '#/components/Prompt' 130 import * as Toast from '#/components/Toast' 131 import {Text as NewText} from '#/components/Typography' 132 + import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 133 import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' 134 import {PostLanguageSelect} from './select-language/PostLanguageSelect' 135 import { ··· 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 }), ··· 366 367 // 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 ) 729 730 + 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> 801 802 <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() 1653 1654 // 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() 1697 1698 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' 13 import {useResolveGifQuery} from '#/state/queries/resolve-link' 14 import {type Gif} from '#/state/queries/tenor' 15 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' ··· 23 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 24 import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif' 25 import {Text} from '#/components/Typography' 26 import {AltTextReminder} from './photos/Gallery' 27 28 export 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' 12 import {useResolveGifQuery} from '#/state/queries/resolve-link' 13 import {type Gif} from '#/state/queries/tenor' 14 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' ··· 22 import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 23 import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif' 24 import {Text} from '#/components/Typography' 25 + import {IS_ANDROID} from '#/env' 26 import {AltTextReminder} from './photos/Gallery' 27 28 export 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
··· 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import type React from 'react' 5 6 - import {isWeb} from '#/platform/detection' 7 import {atoms as a, useTheme} from '#/alf' 8 9 export function KeyboardAccessory({children}: {children: React.ReactNode}) { 10 const t = useTheme() ··· 22 ] 23 24 // 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
··· 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import type React from 'react' 5 6 import {atoms as a, useTheme} from '#/alf' 7 + import {IS_WEB} from '#/env' 8 9 export function KeyboardAccessory({children}: {children: React.ReactNode}) { 10 const t = useTheme() ··· 22 ] 23 24 // 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' 12 import {openUnifiedPicker} from '#/lib/media/picker' 13 import {extractDataUriMime} from '#/lib/media/util' 14 - import {isNative, isWeb} from '#/platform/detection' 15 import {MAX_IMAGES} from '#/view/com/composer/state/composer' 16 import {atoms as a, useTheme} from '#/alf' 17 import {Button} from '#/components/Button' 18 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 19 import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 20 import * as toast from '#/components/Toast' 21 22 export 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) 97 type 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 } 328 329 if (supportedAssets[0].duration) { 330 - if (isWeb) { 331 /* 332 * Web reports duration as seconds 333 */ ··· 432 ) 433 434 const onPressSelectMedia = useCallback(async () => { 435 - if (isNative) { 436 const [photoAccess, videoAccess] = await Promise.all([ 437 requestPhotoAccessIfNeeded(), 438 requestVideoAccessIfNeeded(), ··· 446 } 447 } 448 449 - if (isNative && Keyboard.isVisible()) { 450 Keyboard.dismiss() 451 } 452
··· 11 } from '#/lib/hooks/usePermissions' 12 import {openUnifiedPicker} from '#/lib/media/picker' 13 import {extractDataUriMime} from '#/lib/media/util' 14 import {MAX_IMAGES} from '#/view/com/composer/state/composer' 15 import {atoms as a, useTheme} from '#/alf' 16 import {Button} from '#/components/Button' 17 import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' 18 import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 19 import * as toast from '#/components/Toast' 20 + import {IS_NATIVE, IS_WEB} from '#/env' 21 22 export 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) 97 type 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 } 328 329 if (supportedAssets[0].duration) { 330 + if (IS_WEB) { 331 /* 332 * Web reports duration as seconds 333 */ ··· 432 ) 433 434 const onPressSelectMedia = useCallback(async () => { 435 + if (IS_NATIVE) { 436 const [photoAccess, videoAccess] = await Promise.all([ 437 requestPhotoAccessIfNeeded(), 438 requestVideoAccessIfNeeded(), ··· 446 } 447 } 448 449 + 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' 13 import {atoms as a, useTheme, web} from '#/alf' 14 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 15 import * as Dialog from '#/components/Dialog' ··· 18 import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron' 19 import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield' 20 import {Text} from '#/components/Typography' 21 22 export 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' 12 import {atoms as a, useTheme, web} from '#/alf' 13 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 import * as Dialog from '#/components/Dialog' ··· 17 import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron' 18 import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield' 19 import {Text} from '#/components/Typography' 20 + import {IS_WEB} from '#/env' 21 22 export 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
··· 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 import {type Dimensions} from '#/lib/media/types' 18 import {colors, s} from '#/lib/styles' 19 - import {isNative} from '#/platform/detection' 20 import {type ComposerImage, cropImage} from '#/state/gallery' 21 import {Text} from '#/view/com/util/text/Text' 22 import {tokens, useTheme} from '#/alf' 23 import * as Dialog from '#/components/Dialog' 24 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 25 import {type PostAction} from '../state/composer' 26 import {EditImageDialog} from './EditImageDialog' 27 import {ImageAltTextDialog} from './ImageAltTextDialog' ··· 145 const editControl = Dialog.useDialogControl() 146 147 const onImageEdit = () => { 148 - if (isNative) { 149 cropImage(image).then(next => { 150 onChange(next) 151 })
··· 16 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 17 import {type Dimensions} from '#/lib/media/types' 18 import {colors, s} from '#/lib/styles' 19 import {type ComposerImage, cropImage} from '#/state/gallery' 20 import {Text} from '#/view/com/util/text/Text' 21 import {tokens, useTheme} from '#/alf' 22 import * as Dialog from '#/components/Dialog' 23 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 24 + import {IS_NATIVE} from '#/env' 25 import {type PostAction} from '../state/composer' 26 import {EditImageDialog} from './EditImageDialog' 27 import {ImageAltTextDialog} from './ImageAltTextDialog' ··· 145 const editControl = Dialog.useDialogControl() 146 147 const onImageEdit = () => { 148 + if (IS_NATIVE) { 149 cropImage(image).then(next => { 150 onChange(next) 151 })
+3 -3
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 import {enforceLen} from '#/lib/strings/helpers' 9 - import {isAndroid, isWeb} from '#/platform/detection' 10 import {type ComposerImage} from '#/state/gallery' 11 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 12 import {atoms as a, useTheme} from '#/alf' ··· 16 import * as TextField from '#/components/forms/TextField' 17 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 18 import {Text} from '#/components/Typography' 19 20 type Props = { 21 control: Dialog.DialogOuterProps['control'] ··· 66 const windim = useWindowDimensions() 67 68 const imageStyle = React.useMemo<ImageStyle>(() => { 69 - const maxWidth = isWeb ? 450 : windim.width 70 const source = image.transformed ?? image.source 71 72 if (source.height > source.width) { ··· 165 </AltTextCounterWrapper> 166 </View> 167 {/* Maybe fix this later -h */} 168 - {isAndroid ? <View style={{height: 300}} /> : null} 169 </Dialog.ScrollableInner> 170 ) 171 }
··· 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 import {enforceLen} from '#/lib/strings/helpers' 9 import {type ComposerImage} from '#/state/gallery' 10 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' 11 import {atoms as a, useTheme} from '#/alf' ··· 15 import * as TextField from '#/components/forms/TextField' 16 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 17 import {Text} from '#/components/Typography' 18 + import {IS_ANDROID, IS_WEB} from '#/env' 19 20 type Props = { 21 control: Dialog.DialogOuterProps['control'] ··· 66 const windim = useWindowDimensions() 67 68 const imageStyle = React.useMemo<ImageStyle>(() => { 69 + const maxWidth = IS_WEB ? 450 : windim.width 70 const source = image.transformed ?? image.source 71 72 if (source.height > source.width) { ··· 165 </AltTextCounterWrapper> 166 </View> 167 {/* Maybe fix this later -h */} 168 + {IS_ANDROID ? <View style={{height: 300}} /> : null} 169 </Dialog.ScrollableInner> 170 ) 171 }
+2 -2
src/view/com/composer/photos/OpenCameraBtn.tsx
··· 7 import {useCameraPermission} from '#/lib/hooks/usePermissions' 8 import {openCamera} from '#/lib/media/picker' 9 import {logger} from '#/logger' 10 - import {isMobileWeb, isNative} from '#/platform/detection' 11 import {type ComposerImage, createComposerImage} from '#/state/gallery' 12 import {atoms as a, useTheme} from '#/alf' 13 import {Button} from '#/components/Button' 14 import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' 15 16 type Props = { 17 disabled?: boolean ··· 58 requestMediaPermission, 59 ]) 60 61 - const shouldShowCameraButton = isNative || isMobileWeb 62 if (!shouldShowCameraButton) { 63 return null 64 }
··· 7 import {useCameraPermission} from '#/lib/hooks/usePermissions' 8 import {openCamera} from '#/lib/media/picker' 9 import {logger} from '#/logger' 10 import {type ComposerImage, createComposerImage} from '#/state/gallery' 11 import {atoms as a, useTheme} from '#/alf' 12 import {Button} from '#/components/Button' 13 import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' 14 + import {IS_NATIVE, IS_WEB_MOBILE} from '#/env' 15 16 type Props = { 17 disabled?: boolean ··· 58 requestMediaPermission, 59 ]) 60 61 + const shouldShowCameraButton = IS_NATIVE || IS_WEB_MOBILE 62 if (!shouldShowCameraButton) { 63 return null 64 }
+4 -4
src/view/com/composer/select-language/PostLanguageSelectDialog.tsx
··· 6 7 import {languageName} from '#/locale/helpers' 8 import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' 9 - import {isNative, isWeb} from '#/platform/detection' 10 import { 11 toPostLanguages, 12 useLanguagePrefs, ··· 21 import * as Toggle from '#/components/forms/Toggle' 22 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 23 import {Text} from '#/components/Typography' 24 25 export function PostLanguageSelectDialog({ 26 control, ··· 168 169 const listHeader = ( 170 <View 171 - style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]} 172 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}> 173 <View style={[a.flex_row, a.w_full, a.justify_between]}> 174 <View> ··· 195 </Text> 196 </View> 197 198 - {isWeb && ( 199 <Button 200 variant="ghost" 201 size="small" ··· 252 ListHeaderComponent={listHeader} 253 stickyHeaderIndices={[0]} 254 contentContainerStyle={[a.gap_0]} 255 - style={[isNative && a.px_lg, web({paddingBottom: 120})]} 256 scrollIndicatorInsets={{top: headerHeight}} 257 renderItem={({item, index}) => { 258 if (item.type === 'header') {
··· 6 7 import {languageName} from '#/locale/helpers' 8 import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' 9 import { 10 toPostLanguages, 11 useLanguagePrefs, ··· 20 import * as Toggle from '#/components/forms/Toggle' 21 import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 22 import {Text} from '#/components/Typography' 23 + import {IS_NATIVE, IS_WEB} from '#/env' 24 25 export function PostLanguageSelectDialog({ 26 control, ··· 168 169 const listHeader = ( 170 <View 171 + style={[a.pb_xs, t.atoms.bg, IS_NATIVE && a.pt_2xl]} 172 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}> 173 <View style={[a.flex_row, a.w_full, a.justify_between]}> 174 <View> ··· 195 </Text> 196 </View> 197 198 + {IS_WEB && ( 199 <Button 200 variant="ghost" 201 size="small" ··· 252 ListHeaderComponent={listHeader} 253 stickyHeaderIndices={[0]} 254 contentContainerStyle={[a.gap_0]} 255 + style={[IS_NATIVE && a.px_lg, web({paddingBottom: 120})]} 256 scrollIndicatorInsets={{top: headerHeight}} 257 renderItem={({item, index}) => { 258 if (item.type === 'header') {
+3 -3
src/view/com/composer/text-input/TextInput.tsx
··· 25 import {cleanError} from '#/lib/strings/errors' 26 import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' 27 import {useTheme} from '#/lib/ThemeContext' 28 - import {isAndroid, isNative} from '#/platform/detection' 29 import { 30 type LinkFacetMatch, 31 suggestLinkCardUri, 32 } from '#/view/com/composer/text-input/text-input-util' 33 import {atoms as a, useAlf} from '#/alf' 34 import {normalizeTextStyles} from '#/alf/typography' 35 import {Autocomplete} from './mobile/Autocomplete' 36 import {type TextInputProps} from './TextInput.types' 37 ··· 179 /** 180 * PasteInput doesn't like `lineHeight`, results in jumpiness 181 */ 182 - if (isNative) { 183 style.lineHeight = undefined 184 } 185 186 /* 187 * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant` 188 */ 189 - if (isAndroid) { 190 // @ts-ignore 191 style.fontVariant = style.fontVariant 192 ? style.fontVariant.join(' ')
··· 25 import {cleanError} from '#/lib/strings/errors' 26 import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' 27 import {useTheme} from '#/lib/ThemeContext' 28 import { 29 type LinkFacetMatch, 30 suggestLinkCardUri, 31 } from '#/view/com/composer/text-input/text-input-util' 32 import {atoms as a, useAlf} from '#/alf' 33 import {normalizeTextStyles} from '#/alf/typography' 34 + import {IS_ANDROID, IS_NATIVE} from '#/env' 35 import {Autocomplete} from './mobile/Autocomplete' 36 import {type TextInputProps} from './TextInput.types' 37 ··· 179 /** 180 * PasteInput doesn't like `lineHeight`, results in jumpiness 181 */ 182 + if (IS_NATIVE) { 183 style.lineHeight = undefined 184 } 185 186 /* 187 * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant` 188 */ 189 + if (IS_ANDROID) { 190 // @ts-ignore 191 style.fontVariant = style.fontVariant 192 ? style.fontVariant.join(' ')
+2 -2
src/view/com/composer/threadgate/ThreadgateBtn.tsx
··· 8 9 import {isNetworkError} from '#/lib/strings/errors' 10 import {logger} from '#/logger' 11 - import {isNative} from '#/platform/detection' 12 import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings' 13 import {createPostgateRecord} from '#/state/queries/postgate/util' 14 import {usePreferencesQuery} from '#/state/queries/preferences' ··· 25 import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group' 26 import * as Tooltip from '#/components/Tooltip' 27 import {Text} from '#/components/Typography' 28 import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged' 29 30 export function ThreadgateBtn({ ··· 70 nudged: tooltipWasShown, 71 }) 72 73 - if (isNative && Keyboard.isVisible()) { 74 Keyboard.dismiss() 75 } 76
··· 8 9 import {isNetworkError} from '#/lib/strings/errors' 10 import {logger} from '#/logger' 11 import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings' 12 import {createPostgateRecord} from '#/state/queries/postgate/util' 13 import {usePreferencesQuery} from '#/state/queries/preferences' ··· 24 import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group' 25 import * as Tooltip from '#/components/Tooltip' 26 import {Text} from '#/components/Typography' 27 + import {IS_NATIVE} from '#/env' 28 import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged' 29 30 export function ThreadgateBtn({ ··· 70 nudged: tooltipWasShown, 71 }) 72 73 + if (IS_NATIVE && Keyboard.isVisible()) { 74 Keyboard.dismiss() 75 } 76
+10 -6
src/view/com/composer/videos/SubtitleDialog.tsx
··· 6 import {MAX_ALT_TEXT} from '#/lib/constants' 7 import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' 8 import {LANGUAGES} from '#/locale/languages' 9 - import {isWeb} from '#/platform/detection' 10 import {useLanguagePrefs} from '#/state/preferences' 11 import {atoms as a, useTheme, web} from '#/alf' 12 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 17 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 18 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 19 import {Text} from '#/components/Typography' 20 import {SubtitleFilePicker} from './SubtitleFilePicker' 21 22 const 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>} 56 </ButtonText> 57 </Button> 58 <Dialog.Outer control={control}> ··· 134 </Text> 135 )} 136 137 - {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={() => {
··· 6 import {MAX_ALT_TEXT} from '#/lib/constants' 7 import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' 8 import {LANGUAGES} from '#/locale/languages' 9 import {useLanguagePrefs} from '#/state/preferences' 10 import {atoms as a, useTheme, web} from '#/alf' 11 import {Button, ButtonIcon, ButtonText} from '#/components/Button' ··· 16 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 17 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 18 import {Text} from '#/components/Typography' 19 + import {IS_WEB} from '#/env' 20 import {SubtitleFilePicker} from './SubtitleFilePicker' 21 22 const 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 )} 140 141 + {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={() => {
+2 -2
src/view/com/composer/videos/VideoTranscodeProgress.tsx
··· 4 import {type ImagePickerAsset} from 'expo-image-picker' 5 6 import {clamp} from '#/lib/numbers' 7 - import {isWeb} from '#/platform/detection' 8 import {atoms as a, useTheme} from '#/alf' 9 import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn' 10 import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' 11 ··· 20 }) { 21 const t = useTheme() 22 23 - if (isWeb) return null 24 25 let aspectRatio = asset.width / asset.height 26
··· 4 import {type ImagePickerAsset} from 'expo-image-picker' 5 6 import {clamp} from '#/lib/numbers' 7 import {atoms as a, useTheme} from '#/alf' 8 + import {IS_WEB} from '#/env' 9 import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn' 10 import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' 11 ··· 20 }) { 21 const t = useTheme() 22 23 + if (IS_WEB) return null 24 25 let aspectRatio = asset.width / asset.height 26
+5 -5
src/view/com/feeds/ComposerPrompt.tsx
··· 11 } from '#/lib/hooks/usePermissions' 12 import {openCamera, openUnifiedPicker} from '#/lib/media/picker' 13 import {logger} from '#/logger' 14 - import {isNative} from '#/platform/detection' 15 import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 16 import {MAX_IMAGES} from '#/view/com/composer/state/composer' 17 import {UserAvatar} from '#/view/com/util/UserAvatar' ··· 22 import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 23 import {SubtleHover} from '#/components/SubtleHover' 24 import {Text} from '#/components/Typography' 25 26 export function ComposerPrompt() { 27 const {_} = useLingui() ··· 43 logger.metric('composerPrompt:gallery:press', {}) 44 45 // On web, open the composer with the gallery picker auto-opening 46 - if (!isNative) { 47 openComposer({openGallery: true}) 48 return 49 } ··· 105 return 106 } 107 108 - if (isNative && Keyboard.isVisible()) { 109 Keyboard.dismiss() 110 } 111 ··· 122 ] 123 124 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' 12 import {openCamera, openUnifiedPicker} from '#/lib/media/picker' 13 import {logger} from '#/logger' 14 import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 15 import {MAX_IMAGES} from '#/view/com/composer/state/composer' 16 import {UserAvatar} from '#/view/com/util/UserAvatar' ··· 21 import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 22 import {SubtleHover} from '#/components/SubtleHover' 23 import {Text} from '#/components/Typography' 24 + import {IS_NATIVE} from '#/env' 25 26 export function ComposerPrompt() { 27 const {_} = useLingui() ··· 43 logger.metric('composerPrompt:gallery:press', {}) 44 45 // 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 } 107 108 + if (IS_NATIVE && Keyboard.isVisible()) { 109 Keyboard.dismiss() 110 } 111 ··· 122 ] 123 124 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
··· 20 import {type AllNavigatorParams} from '#/lib/routes/types' 21 import {logEvent} from '#/lib/statsig/statsig' 22 import {s} from '#/lib/styles' 23 - import {isNative} from '#/platform/detection' 24 import {listenSoftReset} from '#/state/events' 25 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 26 import {useSetHomeBadge} from '#/state/home-badge' ··· 34 import {useSession} from '#/state/session' 35 import {useSetMinimalShellMode} from '#/state/shell' 36 import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' 37 import {PostFeed} from '../posts/PostFeed' 38 import {FAB} from '../util/fab/FAB' 39 import {type ListMethods} from '../util/List' ··· 80 const feedIsVideoMode = 81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO 82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode 83 - return isNative && _isVideoFeed 84 }, [feedInfo]) 85 86 useEffect(() => { ··· 91 92 const scrollToTop = useCallback(() => { 93 scrollElRef.current?.scrollToOffset({ 94 - animated: isNative, 95 offset: -headerOffset, 96 }) 97 setMinimalShellMode(false) ··· 136 }) 137 }, [scrollToTop, feed, queryClient]) 138 139 - const shouldPrefetch = isNative && isPageAdjacent 140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI 141 return ( 142 <View
··· 20 import {type AllNavigatorParams} from '#/lib/routes/types' 21 import {logEvent} from '#/lib/statsig/statsig' 22 import {s} from '#/lib/styles' 23 import {listenSoftReset} from '#/state/events' 24 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 25 import {useSetHomeBadge} from '#/state/home-badge' ··· 33 import {useSession} from '#/state/session' 34 import {useSetMinimalShellMode} from '#/state/shell' 35 import {useHeaderOffset} from '#/components/hooks/useHeaderOffset' 36 + import {IS_NATIVE} from '#/env' 37 import {PostFeed} from '../posts/PostFeed' 38 import {FAB} from '../util/fab/FAB' 39 import {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]) 85 86 useEffect(() => { ··· 91 92 const scrollToTop = useCallback(() => { 93 scrollElRef.current?.scrollToOffset({ 94 + animated: IS_NATIVE, 95 offset: -headerOffset, 96 }) 97 setMinimalShellMode(false) ··· 136 }) 137 }, [scrollToTop, feed, queryClient]) 138 139 + 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
··· 4 import {useLingui} from '@lingui/react' 5 6 import {cleanError} from '#/lib/strings/errors' 7 - import {isNative, isWeb} from '#/platform/detection' 8 import {useModerationOpts} from '#/state/preferences/moderation-opts' 9 import {getFeedTypeFromUri} from '#/state/queries/feed' 10 import {useProfileQuery} from '#/state/queries/profile' ··· 15 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 16 import * as ProfileCard from '#/components/ProfileCard' 17 import {Text} from '#/components/Typography' 18 19 export 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()}
··· 4 import {useLingui} from '@lingui/react' 5 6 import {cleanError} from '#/lib/strings/errors' 7 import {useModerationOpts} from '#/state/preferences/moderation-opts' 8 import {getFeedTypeFromUri} from '#/state/queries/feed' 9 import {useProfileQuery} from '#/state/queries/profile' ··· 14 import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 15 import * as ProfileCard from '#/components/ProfileCard' 16 import {Text} from '#/components/Typography' 17 + import {IS_NATIVE, IS_WEB} from '#/env' 18 19 export 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
··· 20 21 import {cleanError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 - import {isIOS, isNative, isWeb} from '#/platform/detection' 24 import {usePreferencesQuery} from '#/state/queries/preferences' 25 import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 26 import {useSession} from '#/state/session' ··· 33 import * as FeedCard from '#/components/FeedCard' 34 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 35 import {ListFooter} from '#/components/Lists' 36 37 const LOADING = {_reactKey: '__loading__'} 38 const EMPTY = {_reactKey: '__empty__'} ··· 111 112 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 ) 219 220 useEffect(() => { 221 - if (isIOS && enabled && scrollElRef.current) { 222 const nativeTag = findNodeHandle(scrollElRef.current) 223 setScrollViewTag(nativeTag) 224 }
··· 20 21 import {cleanError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 import {usePreferencesQuery} from '#/state/queries/preferences' 24 import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 25 import {useSession} from '#/state/session' ··· 32 import * as FeedCard from '#/components/FeedCard' 33 import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag' 34 import {ListFooter} from '#/components/Lists' 35 + import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 36 37 const LOADING = {_reactKey: '__loading__'} 38 const EMPTY = {_reactKey: '__empty__'} ··· 111 112 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 ) 219 220 useEffect(() => { 221 + if (IS_IOS && enabled && scrollElRef.current) { 222 const nativeTag = findNodeHandle(scrollElRef.current) 223 setScrollViewTag(nativeTag) 224 }
+5 -5
src/view/com/lightbox/ImageViewing/index.tsx
··· 41 42 import {type Dimensions} from '#/lib/media/types' 43 import {colors, s} from '#/lib/styles' 44 - import {isIOS} from '#/platform/detection' 45 import {type Lightbox} from '#/state/lightbox' 46 import {Button} from '#/view/com/util/forms/Button' 47 import {Text} from '#/view/com/util/text/Text' 48 import {ScrollView} from '#/view/com/util/Views' 49 import {useTheme} from '#/alf' 50 import {setSystemUITheme} from '#/alf/util/systemUI' 51 import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' 52 import {type ImageSource, type Transform} from './@types' 53 import ImageDefaultHeader from './components/ImageDefaultHeader' ··· 59 const PIXEL_RATIO = PixelRatio.get() 60 61 const SLOW_SPRING: WithSpringConfig = { 62 - mass: isIOS ? 1.25 : 0.75, 63 damping: 300, 64 stiffness: 800, 65 restDisplacementThreshold: 0.01, 66 } 67 const FAST_SPRING: WithSpringConfig = { 68 - mass: isIOS ? 1.25 : 0.75, 69 damping: 150, 70 stiffness: 900, 71 restDisplacementThreshold: 0.01, ··· 248 ) 249 opacity -= dragProgress 250 } 251 - const factor = isIOS ? 100 : 50 252 return { 253 opacity: Math.round(opacity * factor) / factor, 254 } ··· 686 maxHeight: '100%', 687 }, 688 footerText: { 689 - paddingBottom: isIOS ? 20 : 16, 690 }, 691 footerBtns: { 692 flexDirection: 'row',
··· 41 42 import {type Dimensions} from '#/lib/media/types' 43 import {colors, s} from '#/lib/styles' 44 import {type Lightbox} from '#/state/lightbox' 45 import {Button} from '#/view/com/util/forms/Button' 46 import {Text} from '#/view/com/util/text/Text' 47 import {ScrollView} from '#/view/com/util/Views' 48 import {useTheme} from '#/alf' 49 import {setSystemUITheme} from '#/alf/util/systemUI' 50 + import {IS_IOS} from '#/env' 51 import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' 52 import {type ImageSource, type Transform} from './@types' 53 import ImageDefaultHeader from './components/ImageDefaultHeader' ··· 59 const PIXEL_RATIO = PixelRatio.get() 60 61 const SLOW_SPRING: WithSpringConfig = { 62 + mass: IS_IOS ? 1.25 : 0.75, 63 damping: 300, 64 stiffness: 800, 65 restDisplacementThreshold: 0.01, 66 } 67 const FAST_SPRING: WithSpringConfig = { 68 + mass: IS_IOS ? 1.25 : 0.75, 69 damping: 150, 70 stiffness: 900, 71 restDisplacementThreshold: 0.01, ··· 248 ) 249 opacity -= dragProgress 250 } 251 + const factor = IS_IOS ? 100 : 50 252 return { 253 opacity: Math.round(opacity * factor) / factor, 254 } ··· 686 maxHeight: '100%', 687 }, 688 footerText: { 689 + paddingBottom: IS_IOS ? 20 : 16, 690 }, 691 footerBtns: { 692 flexDirection: 'row',
+4 -4
src/view/com/lists/ProfileLists.tsx
··· 20 21 import {cleanError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 - import {isIOS, isNative, isWeb} from '#/platform/detection' 24 import {usePreferencesQuery} from '#/state/queries/preferences' 25 import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 26 import {useSession} from '#/state/session' ··· 33 import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 34 import * as ListCard from '#/components/ListCard' 35 import {ListFooter} from '#/components/Lists' 36 37 const LOADING = {_reactKey: '__loading__'} 38 const EMPTY = {_reactKey: '__empty__'} ··· 111 112 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 ) 218 219 useEffect(() => { 220 - if (isIOS && enabled && scrollElRef.current) { 221 const nativeTag = findNodeHandle(scrollElRef.current) 222 setScrollViewTag(nativeTag) 223 }
··· 20 21 import {cleanError} from '#/lib/strings/errors' 22 import {logger} from '#/logger' 23 import {usePreferencesQuery} from '#/state/queries/preferences' 24 import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 25 import {useSession} from '#/state/session' ··· 32 import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 33 import * as ListCard from '#/components/ListCard' 34 import {ListFooter} from '#/components/Lists' 35 + import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 36 37 const LOADING = {_reactKey: '__loading__'} 38 const EMPTY = {_reactKey: '__empty__'} ··· 111 112 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 ) 218 219 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
··· 16 import {cleanError} from '#/lib/strings/errors' 17 import {colors, gradients, s} from '#/lib/styles' 18 import {useTheme} from '#/lib/ThemeContext' 19 - import {isAndroid, isWeb} from '#/platform/detection' 20 import {useModalControls} from '#/state/modals' 21 import {useAgent, useSession, useSessionApi} from '#/state/session' 22 import {atoms as a, useTheme as useNewTheme} from '#/alf' 23 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 24 import {Text as NewText} from '#/components/Typography' 25 import {resetToTab} from '../../../Navigation' 26 import {ErrorMessage} from '../util/error/ErrorMessage' 27 import {Text} from '../util/text/Text' 28 import * as Toast from '../util/Toast' 29 import {ScrollView, TextInput} from './util' 30 31 - export const snapPoints = isAndroid ? ['90%'] : ['55%'] 32 33 export function Component({}: {}) { 34 const pal = usePalette('default') ··· 173 </> 174 )} 175 176 - <View style={[!isWeb && a.px_xl]}> 177 <View 178 style={[ 179 a.w_full,
··· 16 import {cleanError} from '#/lib/strings/errors' 17 import {colors, gradients, s} from '#/lib/styles' 18 import {useTheme} from '#/lib/ThemeContext' 19 import {useModalControls} from '#/state/modals' 20 import {useAgent, useSession, useSessionApi} from '#/state/session' 21 import {atoms as a, useTheme as useNewTheme} from '#/alf' 22 import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' 23 import {Text as NewText} from '#/components/Typography' 24 + import {IS_ANDROID, IS_WEB} from '#/env' 25 import {resetToTab} from '../../../Navigation' 26 import {ErrorMessage} from '../util/error/ErrorMessage' 27 import {Text} from '../util/text/Text' 28 import * as Toast from '../util/Toast' 29 import {ScrollView, TextInput} from './util' 30 31 + export const snapPoints = IS_ANDROID ? ['90%'] : ['55%'] 32 33 export function Component({}: {}) { 34 const pal = usePalette('default') ··· 173 </> 174 )} 175 176 + <View style={[!IS_WEB && a.px_xl]}> 177 <View 178 style={[ 179 a.w_full,
+5 -5
src/view/com/modals/UserAddRemoveLists.tsx
··· 14 import {cleanError} from '#/lib/strings/errors' 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 import {s} from '#/lib/styles' 17 - import {isAndroid, isMobileWeb, isWeb} from '#/platform/detection' 18 import {useModalControls} from '#/state/modals' 19 import { 20 getMembership, ··· 24 useListMembershipRemoveMutation, 25 } from '#/state/queries/list-memberships' 26 import {useSession} from '#/state/session' 27 import {MyLists} from '../lists/MyLists' 28 import {Button} from '../util/forms/Button' 29 import {Text} from '../util/text/Text' ··· 56 }, [closeModal]) 57 58 const listStyle = React.useMemo(() => { 59 - if (isMobileWeb) { 60 return [pal.border, {height: screenHeight / 2}] 61 - } else if (isWeb) { 62 return [pal.border, {height: screenHeight / 1.5}] 63 } 64 ··· 243 244 const styles = StyleSheet.create({ 245 container: { 246 - paddingHorizontal: isWeb ? 0 : 16, 247 }, 248 btns: { 249 position: 'relative', ··· 252 justifyContent: 'center', 253 gap: 10, 254 paddingTop: 10, 255 - paddingBottom: isAndroid ? 10 : 0, 256 borderTopWidth: StyleSheet.hairlineWidth, 257 }, 258 footerBtn: {
··· 14 import {cleanError} from '#/lib/strings/errors' 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 import {s} from '#/lib/styles' 17 import {useModalControls} from '#/state/modals' 18 import { 19 getMembership, ··· 23 useListMembershipRemoveMutation, 24 } from '#/state/queries/list-memberships' 25 import {useSession} from '#/state/session' 26 + import {IS_ANDROID, IS_WEB, IS_WEB_MOBILE} from '#/env' 27 import {MyLists} from '../lists/MyLists' 28 import {Button} from '../util/forms/Button' 29 import {Text} from '../util/text/Text' ··· 56 }, [closeModal]) 57 58 const listStyle = React.useMemo(() => { 59 + if (IS_WEB_MOBILE) { 60 return [pal.border, {height: screenHeight / 2}] 61 + } else if (IS_WEB) { 62 return [pal.border, {height: screenHeight / 1.5}] 63 } 64 ··· 243 244 const styles = StyleSheet.create({ 245 container: { 246 + paddingHorizontal: IS_WEB ? 0 : 16, 247 }, 248 btns: { 249 position: 'relative', ··· 252 justifyContent: 'center', 253 gap: 10, 254 paddingTop: 10, 255 + paddingBottom: IS_ANDROID ? 10 : 0, 256 borderTopWidth: StyleSheet.hairlineWidth, 257 }, 258 footerBtn: {
+2 -2
src/view/com/pager/PagerHeaderContext.tsx
··· 1 import React, {useContext} from 'react' 2 import {type SharedValue} from 'react-native-reanimated' 3 4 - import {isNative} from '#/platform/detection' 5 6 export const PagerHeaderContext = React.createContext<{ 7 scrollY: SharedValue<number> ··· 37 38 export 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',
··· 1 import React, {useContext} from 'react' 2 import {type SharedValue} from 'react-native-reanimated' 3 4 + import {IS_NATIVE} from '#/env' 5 6 export const PagerHeaderContext = React.createContext<{ 7 scrollY: SharedValue<number> ··· 37 38 export 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
··· 18 19 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 20 import {ScrollProvider} from '#/lib/ScrollContext' 21 - import {isIOS} from '#/platform/detection' 22 import { 23 Pager, 24 type PagerRef, 25 type RenderTabBarFnProps, 26 } from '#/view/com/pager/Pager' 27 import {useTheme} from '#/alf' 28 import {type ListMethods} from '../util/List' 29 import {PagerHeaderProvider} from './PagerHeaderContext' 30 import {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 {
··· 18 19 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 20 import {ScrollProvider} from '#/lib/ScrollContext' 21 import { 22 Pager, 23 type PagerRef, 24 type RenderTabBarFnProps, 25 } from '#/view/com/pager/Pager' 26 import {useTheme} from '#/alf' 27 + import {IS_IOS} from '#/env' 28 import {type ListMethods} from '../util/List' 29 import {PagerHeaderProvider} from './PagerHeaderContext' 30 import {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
··· 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {s} from '#/lib/styles' 15 import {logger} from '#/logger' 16 - import {isWeb} from '#/platform/detection' 17 import {useFeedFeedbackContext} from '#/state/feed-feedback' 18 import {useSession} from '#/state/session' 19 import {Button} from '../util/forms/Button' 20 import {Text} from '../util/text/Text' 21 ··· 44 const navigation = useNavigation<NavigationProp>() 45 46 const onPressFindAccounts = React.useCallback(() => { 47 - if (isWeb) { 48 navigation.navigate('Search', {}) 49 } else { 50 navigation.navigate('SearchTab')
··· 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {s} from '#/lib/styles' 15 import {logger} from '#/logger' 16 import {useFeedFeedbackContext} from '#/state/feed-feedback' 17 import {useSession} from '#/state/session' 18 + import {IS_WEB} from '#/env' 19 import {Button} from '../util/forms/Button' 20 import {Text} from '../util/text/Text' 21 ··· 44 const navigation = useNavigation<NavigationProp>() 45 46 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
··· 11 import {MagnifyingGlassIcon} from '#/lib/icons' 12 import {type NavigationProp} from '#/lib/routes/types' 13 import {s} from '#/lib/styles' 14 - import {isWeb} from '#/platform/detection' 15 import {Button} from '../util/forms/Button' 16 import {Text} from '../util/text/Text' 17 ··· 21 const navigation = useNavigation<NavigationProp>() 22 23 const onPressFindAccounts = React.useCallback(() => { 24 - if (isWeb) { 25 navigation.navigate('Search', {}) 26 } else { 27 navigation.navigate('SearchTab')
··· 11 import {MagnifyingGlassIcon} from '#/lib/icons' 12 import {type NavigationProp} from '#/lib/routes/types' 13 import {s} from '#/lib/styles' 14 + import {IS_WEB} from '#/env' 15 import {Button} from '../util/forms/Button' 16 import {Text} from '../util/text/Text' 17 ··· 21 const navigation = useNavigation<NavigationProp>() 22 23 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
··· 10 import {usePalette} from '#/lib/hooks/usePalette' 11 import {type NavigationProp} from '#/lib/routes/types' 12 import {s} from '#/lib/styles' 13 - import {isWeb} from '#/platform/detection' 14 import {Button} from '../util/forms/Button' 15 import {Text} from '../util/text/Text' 16 ··· 20 const navigation = useNavigation<NavigationProp>() 21 22 const onPressFindAccounts = React.useCallback(() => { 23 - if (isWeb) { 24 navigation.navigate('Search', {}) 25 } else { 26 navigation.navigate('SearchTab')
··· 10 import {usePalette} from '#/lib/hooks/usePalette' 11 import {type NavigationProp} from '#/lib/routes/types' 12 import {s} from '#/lib/styles' 13 + import {IS_WEB} from '#/env' 14 import {Button} from '../util/forms/Button' 15 import {Text} from '../util/text/Text' 16 ··· 20 const navigation = useNavigation<NavigationProp>() 21 22 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
··· 34 import {logEvent, useGate} from '#/lib/statsig/statsig' 35 import {isNetworkError} from '#/lib/strings/errors' 36 import {logger} from '#/logger' 37 - import {isIOS, isNative, isWeb} from '#/platform/detection' 38 import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow' 39 import {listenPostCreated} from '#/state/events' 40 import {useFeedFeedbackContext} from '#/state/feed-feedback' ··· 70 } from '#/components/feeds/PostFeedVideoGridRow' 71 import {TrendingInterstitial} from '#/components/interstitials/Trending' 72 import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' 73 import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' 74 import {ComposerPrompt} from '../feeds/ComposerPrompt' 75 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' ··· 243 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') 244 const {gtMobile} = useBreakpoints() 245 const {rightNavVisible} = useLayoutBreakpoints() 246 - const areVideoFeedsEnabled = isNative 247 248 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) 877 878 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 />
··· 34 import {logEvent, useGate} from '#/lib/statsig/statsig' 35 import {isNetworkError} from '#/lib/strings/errors' 36 import {logger} from '#/logger' 37 import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow' 38 import {listenPostCreated} from '#/state/events' 39 import {useFeedFeedbackContext} from '#/state/feed-feedback' ··· 69 } from '#/components/feeds/PostFeedVideoGridRow' 70 import {TrendingInterstitial} from '#/components/interstitials/Trending' 71 import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' 72 + import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' 73 import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' 74 import {ComposerPrompt} from '../feeds/ComposerPrompt' 75 import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' ··· 243 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|') 244 const {gtMobile} = useBreakpoints() 245 const {rightNavVisible} = useLayoutBreakpoints() 246 + const areVideoFeedsEnabled = IS_NATIVE 247 248 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) 877 878 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
··· 8 import {type NavigationProp} from '#/lib/routes/types' 9 import {cleanError} from '#/lib/strings/errors' 10 import {logger} from '#/logger' 11 - import {isWeb} from '#/platform/detection' 12 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 13 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 14 import {useSession} from '#/state/session' 15 import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX' 16 import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' 17 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 18 import {List} from '../util/List' 19 import {ProfileCardWithFollowBtn} from './ProfileCard' 20 ··· 49 const navigation = useNavigation<NavigationProp>() 50 51 const onPressFindAccounts = React.useCallback(() => { 52 - if (isWeb) { 53 navigation.navigate('Search', {}) 54 } else { 55 navigation.navigate('SearchTab')
··· 8 import {type NavigationProp} from '#/lib/routes/types' 9 import {cleanError} from '#/lib/strings/errors' 10 import {logger} from '#/logger' 11 import {useProfileFollowsQuery} from '#/state/queries/profile-follows' 12 import {useResolveDidQuery} from '#/state/queries/resolve-uri' 13 import {useSession} from '#/state/session' 14 import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX' 15 import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2' 16 import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 17 + import {IS_WEB} from '#/env' 18 import {List} from '../util/List' 19 import {ProfileCardWithFollowBtn} from './ProfileCard' 20 ··· 49 const navigation = useNavigation<NavigationProp>() 50 51 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
··· 12 import {shareText, shareUrl} from '#/lib/sharing' 13 import {toShareUrl} from '#/lib/strings/url-helpers' 14 import {logger} from '#/logger' 15 - import {isWeb} from '#/platform/detection' 16 import {type Shadow} from '#/state/cache/types' 17 import {useModalControls} from '#/state/modals' 18 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' ··· 61 import {useFullVerificationState} from '#/components/verification' 62 import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' 63 import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' 64 import {Dot} from '#/features/nuxs/components/Dot' 65 import {Gradient} from '#/features/nuxs/components/Gradient' 66 import {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} /> 286 </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>
··· 12 import {shareText, shareUrl} from '#/lib/sharing' 13 import {toShareUrl} from '#/lib/strings/url-helpers' 14 import {logger} from '#/logger' 15 import {type Shadow} from '#/state/cache/types' 16 import {useModalControls} from '#/state/modals' 17 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' ··· 60 import {useFullVerificationState} from '#/components/verification' 61 import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' 62 import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' 63 + import {IS_WEB} from '#/env' 64 import {Dot} from '#/features/nuxs/components/Dot' 65 import {Gradient} from '#/features/nuxs/components/Gradient' 66 import {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' 27 import {type TypographyVariant} from '#/lib/ThemeContext' 28 - import {isAndroid, isWeb} from '#/platform/detection' 29 import {emitSoftReset} from '#/state/events' 30 import {useModalControls} from '#/state/modals' 31 import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper' 32 import {useTheme} from '#/alf' 33 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 34 import {router} from '../../../routes' 35 import {PressableWithHover} from './PressableWithHover' 36 import {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' 27 import {type TypographyVariant} from '#/lib/ThemeContext' 28 import {emitSoftReset} from '#/state/events' 29 import {useModalControls} from '#/state/modals' 30 import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper' 31 import {useTheme} from '#/alf' 32 import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 33 + import {IS_ANDROID, IS_WEB} from '#/env' 34 import {router} from '../../../routes' 35 import {PressableWithHover} from './PressableWithHover' 36 import {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
··· 11 import {useDedupe} from '#/lib/hooks/useDedupe' 12 import {useScrollHandlers} from '#/lib/ScrollContext' 13 import {addStyle} from '#/lib/styles' 14 - import {isIOS} from '#/platform/detection' 15 import {useLightbox} from '#/state/lightbox' 16 import {useTheme} from '#/alf' 17 import {FlatList_INTERNAL} from './Views' 18 19 export type ListMethods = FlatList_INTERNAL ··· 94 } 95 } 96 97 - if (isIOS) { 98 runOnJS(dedupe)(updateActiveVideoViewAsync) 99 } 100 }, ··· 184 185 // 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 188 function useAllowScrollToTopIOS() { 189 const {activeLightbox} = useLightbox() 190 return !activeLightbox
··· 11 import {useDedupe} from '#/lib/hooks/useDedupe' 12 import {useScrollHandlers} from '#/lib/ScrollContext' 13 import {addStyle} from '#/lib/styles' 14 import {useLightbox} from '#/state/lightbox' 15 import {useTheme} from '#/alf' 16 + import {IS_IOS} from '#/env' 17 import {FlatList_INTERNAL} from './Views' 18 19 export type ListMethods = FlatList_INTERNAL ··· 94 } 95 } 96 97 + if (IS_IOS) { 98 runOnJS(dedupe)(updateActiveVideoViewAsync) 99 } 100 }, ··· 184 185 // 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 188 function useAllowScrollToTopIOS() { 189 const {activeLightbox} = useLightbox() 190 return !activeLightbox
+8 -8
src/view/com/util/MainScrollProvider.tsx
··· 9 import EventEmitter from 'eventemitter3' 10 11 import {ScrollProvider} from '#/lib/ScrollContext' 12 - import {isNative, isWeb} from '#/platform/detection' 13 import {useMinimalShellMode} from '#/state/shell' 14 import {useShellLayout} from '#/state/shell/shell-layout' 15 16 const WEB_HIDE_SHELL_THRESHOLD = 200 17 ··· 35 ) 36 37 useEffect(() => { 38 - if (isWeb) { 39 return listenToForcedWindowScroll(() => { 40 startDragOffset.set(null) 41 startMode.set(null) ··· 48 (e: NativeScrollEvent) => { 49 'worklet' 50 const offsetY = Math.max(0, e.contentOffset.y) 51 - if (isNative) { 52 const startDragOffsetValue = startDragOffset.get() 53 if (startDragOffsetValue === null) { 54 return ··· 75 (e: NativeScrollEvent) => { 76 'worklet' 77 const offsetY = Math.max(0, e.contentOffset.y) 78 - if (isNative) { 79 startDragOffset.set(offsetY) 80 startMode.set(headerMode.get()) 81 } ··· 86 const onEndDrag = useCallback( 87 (e: NativeScrollEvent) => { 88 'worklet' 89 - if (isNative) { 90 if (e.velocity && e.velocity.y !== 0) { 91 // If we detect a velocity, wait for onMomentumEnd to snap. 92 return ··· 100 const onMomentumEnd = useCallback( 101 (e: NativeScrollEvent) => { 102 'worklet' 103 - if (isNative) { 104 snapToClosestState(e) 105 } 106 }, ··· 111 (e: NativeScrollEvent) => { 112 'worklet' 113 const offsetY = Math.max(0, e.contentOffset.y) 114 - if (isNative) { 115 const startDragOffsetValue = startDragOffset.get() 116 const startModeValue = startMode.get() 117 if (startDragOffsetValue === null || startModeValue === null) { ··· 177 178 const emitter = new EventEmitter() 179 180 - if (isWeb) { 181 const originalScroll = window.scroll 182 window.scroll = function () { 183 emitter.emit('forced-scroll')
··· 9 import EventEmitter from 'eventemitter3' 10 11 import {ScrollProvider} from '#/lib/ScrollContext' 12 import {useMinimalShellMode} from '#/state/shell' 13 import {useShellLayout} from '#/state/shell/shell-layout' 14 + import {IS_NATIVE, IS_WEB} from '#/env' 15 16 const WEB_HIDE_SHELL_THRESHOLD = 200 17 ··· 35 ) 36 37 useEffect(() => { 38 + if (IS_WEB) { 39 return listenToForcedWindowScroll(() => { 40 startDragOffset.set(null) 41 startMode.set(null) ··· 48 (e: NativeScrollEvent) => { 49 'worklet' 50 const offsetY = Math.max(0, e.contentOffset.y) 51 + if (IS_NATIVE) { 52 const startDragOffsetValue = startDragOffset.get() 53 if (startDragOffsetValue === null) { 54 return ··· 75 (e: NativeScrollEvent) => { 76 'worklet' 77 const offsetY = Math.max(0, e.contentOffset.y) 78 + if (IS_NATIVE) { 79 startDragOffset.set(offsetY) 80 startMode.set(headerMode.get()) 81 } ··· 86 const onEndDrag = useCallback( 87 (e: NativeScrollEvent) => { 88 'worklet' 89 + if (IS_NATIVE) { 90 if (e.velocity && e.velocity.y !== 0) { 91 // If we detect a velocity, wait for onMomentumEnd to snap. 92 return ··· 100 const onMomentumEnd = useCallback( 101 (e: NativeScrollEvent) => { 102 'worklet' 103 + if (IS_NATIVE) { 104 snapToClosestState(e) 105 } 106 }, ··· 111 (e: NativeScrollEvent) => { 112 'worklet' 113 const offsetY = Math.max(0, e.contentOffset.y) 114 + if (IS_NATIVE) { 115 const startDragOffsetValue = startDragOffset.get() 116 const startModeValue = startMode.get() 117 if (startDragOffsetValue === null || startModeValue === null) { ··· 177 178 const emitter = new EventEmitter() 179 180 + if (IS_WEB) { 181 const originalScroll = window.scroll 182 window.scroll = function () { 183 emitter.emit('forced-scroll')
+3 -3
src/view/com/util/PostMeta.tsx
··· 12 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 import {sanitizeHandle} from '#/lib/strings/handles' 14 import {niceDate} from '#/lib/strings/time' 15 - import {isAndroid} from '#/platform/detection' 16 import {useProfileShadow} from '#/state/cache/profile-shadow' 17 import {precacheProfile} from '#/state/queries/profile' 18 import {atoms as a, platform, useTheme, web} from '#/alf' ··· 21 import {Text} from '#/components/Typography' 22 import {useSimpleVerificationState} from '#/components/verification' 23 import {VerificationCheck} from '#/components/verification/VerificationCheck' 24 import {TimeElapsed} from './TimeElapsed' 25 import {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,
··· 12 import {sanitizeDisplayName} from '#/lib/strings/display-names' 13 import {sanitizeHandle} from '#/lib/strings/handles' 14 import {niceDate} from '#/lib/strings/time' 15 import {useProfileShadow} from '#/state/cache/profile-shadow' 16 import {precacheProfile} from '#/state/queries/profile' 17 import {atoms as a, platform, useTheme, web} from '#/alf' ··· 20 import {Text} from '#/components/Typography' 21 import {useSimpleVerificationState} from '#/components/verification' 22 import {VerificationCheck} from '#/components/verification/VerificationCheck' 23 + import {IS_ANDROID} from '#/env' 24 import {TimeElapsed} from './TimeElapsed' 25 import {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
··· 30 import {isCancelledError} from '#/lib/strings/errors' 31 import {sanitizeHandle} from '#/lib/strings/handles' 32 import {logger} from '#/logger' 33 - import {isAndroid, isNative, isWeb} from '#/platform/detection' 34 import { 35 type ComposerImage, 36 compressImage, ··· 54 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 55 import * as Menu from '#/components/Menu' 56 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 57 import type * as bsky from '#/types/bsky' 58 59 export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' ··· 88 onBeforePress?: () => void 89 } 90 91 - const BLUR_AMOUNT = isWeb ? 5 : 100 92 93 let DefaultAvatar = ({ 94 type, ··· 286 }, [size, style]) 287 288 return avatar && 289 - !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( 290 <View style={containerStyle}> 291 {usePlainRNImage ? ( 292 <RNImage ··· 394 } 395 396 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={_(
··· 30 import {isCancelledError} from '#/lib/strings/errors' 31 import {sanitizeHandle} from '#/lib/strings/handles' 32 import {logger} from '#/logger' 33 import { 34 type ComposerImage, 35 compressImage, ··· 53 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 54 import * as Menu from '#/components/Menu' 55 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 56 + import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' 57 import type * as bsky from '#/types/bsky' 58 59 export type UserAvatarType = 'user' | 'algo' | 'list' | 'labeler' ··· 88 onBeforePress?: () => void 89 } 90 91 + const BLUR_AMOUNT = IS_WEB ? 5 : 100 92 93 let DefaultAvatar = ({ 94 type, ··· 286 }, [size, style]) 287 288 return avatar && 289 + !((moderation?.blur && IS_ANDROID) /* android crashes with blur */) ? ( 290 <View style={containerStyle}> 291 {usePlainRNImage ? ( 292 <RNImage ··· 394 } 395 396 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
··· 14 import {type PickerImage} from '#/lib/media/picker.shared' 15 import {isCancelledError} from '#/lib/strings/errors' 16 import {logger} from '#/logger' 17 - import {isAndroid, isNative} from '#/platform/detection' 18 import { 19 type ComposerImage, 20 compressImage, ··· 32 import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' 33 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 34 import * as Menu from '#/components/Menu' 35 36 export function UserBanner({ 37 type, ··· 75 } 76 77 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]}
··· 14 import {type PickerImage} from '#/lib/media/picker.shared' 15 import {isCancelledError} from '#/lib/strings/errors' 16 import {logger} from '#/logger' 17 import { 18 type ComposerImage, 19 compressImage, ··· 31 import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive' 32 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 33 import * as Menu from '#/components/Menu' 34 + import {IS_ANDROID, IS_NATIVE} from '#/env' 35 36 export function UserBanner({ 37 type, ··· 75 } 76 77 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
··· 13 import {usePalette} from '#/lib/hooks/usePalette' 14 import {clamp} from '#/lib/numbers' 15 import {colors, s} from '#/lib/styles' 16 - import {isAndroid} from '#/platform/detection' 17 import {Text} from './text/Text' 18 import {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={
··· 13 import {usePalette} from '#/lib/hooks/usePalette' 14 import {clamp} from '#/lib/numbers' 15 import {colors, s} from '#/lib/styles' 16 + import {IS_ANDROID} from '#/env' 17 import {Text} from './text/Text' 18 import {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
··· 12 import {useHaptics} from '#/lib/haptics' 13 import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' 14 import {clamp} from '#/lib/numbers' 15 - import {isWeb} from '#/platform/detection' 16 import {ios, useBreakpoints, useTheme} from '#/alf' 17 import {atoms as a} from '#/alf' 18 19 export 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 },
··· 12 import {useHaptics} from '#/lib/haptics' 13 import {useMinimalShellFabTransform} from '#/lib/hooks/useMinimalShellTransform' 14 import {clamp} from '#/lib/numbers' 15 import {ios, useBreakpoints, useTheme} from '#/alf' 16 import {atoms as a} from '#/alf' 17 + import {IS_WEB} from '#/env' 18 19 export 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
··· 5 import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 6 import {usePalette} from '#/lib/hooks/usePalette' 7 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 - import {isWeb} from '#/platform/detection' 9 import {atoms as a} from '#/alf' 10 import {Text} from '../text/Text' 11 12 export 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>
··· 5 import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 6 import {usePalette} from '#/lib/hooks/usePalette' 7 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 8 import {atoms as a} from '#/alf' 9 + import {IS_WEB} from '#/env' 10 import {Text} from '../text/Text' 11 12 export 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
··· 5 import {lh, s} from '#/lib/styles' 6 import {type TypographyVariant, useTheme} from '#/lib/ThemeContext' 7 import {logger} from '#/logger' 8 - import {isIOS, isWeb} from '#/platform/detection' 9 import {applyFonts, useAlf} from '#/alf' 10 import { 11 childHasEmoji, 12 renderChildrenWithEmoji, 13 type StringChild, 14 } from '#/alf/typography' 15 16 export type CustomTextProps = Omit<TextProps, 'children'> & { 17 type?: TypographyVariant ··· 81 } 82 83 return { 84 - uiTextView: selectable && isIOS, 85 selectable, 86 style: flattened, 87 - dataSet: isWeb 88 ? Object.assign({tooltip: title}, dataSet || {}) 89 : undefined, 90 ...props,
··· 5 import {lh, s} from '#/lib/styles' 6 import {type TypographyVariant, useTheme} from '#/lib/ThemeContext' 7 import {logger} from '#/logger' 8 import {applyFonts, useAlf} from '#/alf' 9 import { 10 childHasEmoji, 11 renderChildrenWithEmoji, 12 type StringChild, 13 } from '#/alf/typography' 14 + import {IS_IOS, IS_WEB} from '#/env' 15 16 export type CustomTextProps = Omit<TextProps, 'children'> & { 17 type?: TypographyVariant ··· 81 } 82 83 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' 17 import {cleanError} from '#/lib/strings/errors' 18 import {s} from '#/lib/styles' 19 - import {isNative, isWeb} from '#/platform/detection' 20 import { 21 type SavedFeedItem, 22 useGetPopularFeedsQuery, ··· 46 import * as Layout from '#/components/Layout' 47 import {Link} from '#/components/Link' 48 import * as ListCard from '#/components/ListCard' 49 50 type 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' 17 import {cleanError} from '#/lib/strings/errors' 18 import {s} from '#/lib/styles' 19 import { 20 type SavedFeedItem, 21 useGetPopularFeedsQuery, ··· 45 import * as Layout from '#/components/Layout' 46 import {Link} from '#/components/Link' 47 import * as ListCard from '#/components/ListCard' 48 + import {IS_NATIVE, IS_WEB} from '#/env' 49 50 type 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' 14 import {logEvent} from '#/lib/statsig/statsig' 15 - import {isWeb} from '#/platform/detection' 16 import {emitSoftReset} from '#/state/events' 17 import { 18 type SavedFeedSourceInfo, ··· 37 import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 38 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 39 import * as Layout from '#/components/Layout' 40 import {useDemoMode} from '#/storage/hooks/demo-mode' 41 42 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> ··· 48 usePinnedFeedsInfos() 49 50 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' 14 import {logEvent} from '#/lib/statsig/statsig' 15 import {emitSoftReset} from '#/state/events' 16 import { 17 type SavedFeedSourceInfo, ··· 36 import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' 37 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 38 import * as Layout from '#/components/Layout' 39 + import {IS_WEB} from '#/env' 40 import {useDemoMode} from '#/storage/hooks/demo-mode' 41 42 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> ··· 48 usePinnedFeedsInfos() 49 50 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' 15 import {s} from '#/lib/styles' 16 import {logger} from '#/logger' 17 - import {isNative} from '#/platform/detection' 18 import {emitSoftReset, listenSoftReset} from '#/state/events' 19 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 20 import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' ··· 39 import * as Layout from '#/components/Layout' 40 import {InlineLinkText, Link} from '#/components/Link' 41 import {Loader} from '#/components/Loader' 42 43 // 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' 15 import {s} from '#/lib/styles' 16 import {logger} from '#/logger' 17 import {emitSoftReset, listenSoftReset} from '#/state/events' 18 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 19 import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings' ··· 38 import * as Layout from '#/components/Layout' 39 import {InlineLinkText, Link} from '#/components/Link' 40 import {Loader} from '#/components/Loader' 41 + import {IS_NATIVE} from '#/env' 42 43 // 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
··· 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 import {colors} from '#/lib/styles' 16 - import {isWeb} from '#/platform/detection' 17 import {emitSoftReset} from '#/state/events' 18 import {useKawaiiMode} from '#/state/preferences/kawaii' 19 import {useUnreadNotifications} from '#/state/queries/notifications/unread' ··· 55 import {Text} from '#/components/Typography' 56 import {useSimpleVerificationState} from '#/components/verification' 57 import {VerificationCheck} from '#/components/verification/VerificationCheck' 58 59 const 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})
··· 13 import {type NavigationProp} from '#/lib/routes/types' 14 import {sanitizeHandle} from '#/lib/strings/handles' 15 import {colors} from '#/lib/styles' 16 import {emitSoftReset} from '#/state/events' 17 import {useKawaiiMode} from '#/state/preferences/kawaii' 18 import {useUnreadNotifications} from '#/state/queries/notifications/unread' ··· 54 import {Text} from '#/components/Typography' 55 import {useSimpleVerificationState} from '#/components/verification' 56 import {VerificationCheck} from '#/components/verification/VerificationCheck' 57 + import {IS_WEB} from '#/env' 58 59 const 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})
+3 -3
src/view/shell/createNativeStackNavigatorWithAuth.tsx
··· 27 28 import {PWI_ENABLED} from '#/lib/build-flags' 29 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 30 - import {isNative, isWeb} from '#/platform/detection' 31 import {useSession} from '#/state/session' 32 import {useOnboardingState} from '#/state/shell' 33 import { ··· 39 import {SignupQueued} from '#/screens/SignupQueued' 40 import {atoms as a, useLayoutBreakpoints} from '#/alf' 41 import {PolicyUpdateOverlay} from '#/components/PolicyUpdateOverlay' 42 import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 43 import {DesktopLeftNav} from './desktop/LeftNav' 44 import {DesktopRightNav} from './desktop/RightNav' ··· 115 const {setShowLoggedOut} = useLoggedOutViewControls() 116 const {isMobile} = useWebMediaQueries() 117 const {leftNavMinimal} = useLayoutBreakpoints() 118 - if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || isNative)) { 119 return <LoggedOut /> 120 } 121 if (hasSession && currentAccount?.signupQueued) { ··· 158 describe={describe} 159 /> 160 </View> 161 - {isWeb && ( 162 <> 163 {showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />} 164 {!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
··· 27 28 import {PWI_ENABLED} from '#/lib/build-flags' 29 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 30 import {useSession} from '#/state/session' 31 import {useOnboardingState} from '#/state/shell' 32 import { ··· 38 import {SignupQueued} from '#/screens/SignupQueued' 39 import {atoms as a, useLayoutBreakpoints} from '#/alf' 40 import {PolicyUpdateOverlay} from '#/components/PolicyUpdateOverlay' 41 + import {IS_NATIVE, IS_WEB} from '#/env' 42 import {BottomBarWeb} from './bottom-bar/BottomBarWeb' 43 import {DesktopLeftNav} from './desktop/LeftNav' 44 import {DesktopRightNav} from './desktop/RightNav' ··· 115 const {setShowLoggedOut} = useLoggedOutViewControls() 116 const {isMobile} = useWebMediaQueries() 117 const {leftNavMinimal} = useLayoutBreakpoints() 118 + if (!hasSession && (!PWI_ENABLED || activeRouteRequiresAuth || IS_NATIVE)) { 119 return <LoggedOut /> 120 } 121 if (hasSession && currentAccount?.signupQueued) { ··· 158 describe={describe} 159 /> 160 </View> 161 + {IS_WEB && ( 162 <> 163 {showBottomBar ? <BottomBarWeb /> : <DesktopLeftNav />} 164 {!isMobile && <DesktopRightNav routeName={activeRoute.name} />}
+8 -6
src/view/shell/index.tsx
··· 11 import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' 12 import {useNotificationsRegistration} from '#/lib/notifications/notifications' 13 import {isStateAtTabRoot} from '#/lib/routes/helpers' 14 - import {isAndroid, isIOS} from '#/platform/detection' 15 import {useDialogFullyExpandedCountContext} from '#/state/dialogs' 16 import {useSession} from '#/state/session' 17 import { ··· 43 import {useAgeAssurance} from '#/ageAssurance' 44 import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' 45 import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' 46 import {RoutesContainer, TabsNavigator} from '#/Navigation' 47 import {BottomSheetOutlet} from '../../../modules/bottom-sheet' 48 import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' ··· 60 useNotificationsHandler() 61 62 useEffect(() => { 63 - if (isAndroid) { 64 const listener = BackHandler.addEventListener('hardwareBackPress', () => { 65 return closeAnyActiveElement() 66 }) ··· 80 const navigation = useNavigation() 81 const dedupe = useDedupe(1000) 82 useEffect(() => { 83 - if (!isAndroid) return 84 const onFocusOrBlur = () => { 85 setTimeout(() => { 86 dedupe(updateActiveViewAsync) ··· 190 swipeEdgeWidth={winDim.width} 191 swipeMinVelocity={100} 192 swipeMinDistance={10} 193 - drawerType={isIOS ? 'slide' : 'front'} 194 overlayStyle={{ 195 backgroundColor: select(t.name, { 196 light: 'rgba(0, 57, 117, 0.1)', 197 - dark: isAndroid ? 'rgba(16, 133, 254, 0.1)' : 'rgba(1, 82, 168, 0.1)', 198 dim: 'rgba(10, 13, 16, 0.8)', 199 }), 200 }}> ··· 220 <SystemBars 221 style={{ 222 statusBar: 223 - t.name !== 'light' || (isIOS && fullyExpandedCount > 0) 224 ? 'light' 225 : 'dark', 226 navigationBar: t.name !== 'light' ? 'light' : 'dark',
··· 11 import {useNotificationsHandler} from '#/lib/hooks/useNotificationHandler' 12 import {useNotificationsRegistration} from '#/lib/notifications/notifications' 13 import {isStateAtTabRoot} from '#/lib/routes/helpers' 14 import {useDialogFullyExpandedCountContext} from '#/state/dialogs' 15 import {useSession} from '#/state/session' 16 import { ··· 42 import {useAgeAssurance} from '#/ageAssurance' 43 import {NoAccessScreen} from '#/ageAssurance/components/NoAccessScreen' 44 import {RedirectOverlay} from '#/ageAssurance/components/RedirectOverlay' 45 + import {IS_ANDROID, IS_IOS} from '#/env' 46 import {RoutesContainer, TabsNavigator} from '#/Navigation' 47 import {BottomSheetOutlet} from '../../../modules/bottom-sheet' 48 import {updateActiveViewAsync} from '../../../modules/expo-bluesky-swiss-army/src/VisibilityView' ··· 60 useNotificationsHandler() 61 62 useEffect(() => { 63 + if (IS_ANDROID) { 64 const listener = BackHandler.addEventListener('hardwareBackPress', () => { 65 return closeAnyActiveElement() 66 }) ··· 80 const navigation = useNavigation() 81 const dedupe = useDedupe(1000) 82 useEffect(() => { 83 + if (!IS_ANDROID) return 84 const onFocusOrBlur = () => { 85 setTimeout(() => { 86 dedupe(updateActiveViewAsync) ··· 190 swipeEdgeWidth={winDim.width} 191 swipeMinVelocity={100} 192 swipeMinDistance={10} 193 + drawerType={IS_IOS ? 'slide' : 'front'} 194 overlayStyle={{ 195 backgroundColor: select(t.name, { 196 light: 'rgba(0, 57, 117, 0.1)', 197 + dark: IS_ANDROID 198 + ? 'rgba(16, 133, 254, 0.1)' 199 + : 'rgba(1, 82, 168, 0.1)', 200 dim: 'rgba(10, 13, 16, 0.8)', 201 }), 202 }}> ··· 222 <SystemBars 223 style={{ 224 statusBar: 225 + t.name !== 'light' || (IS_IOS && fullyExpandedCount > 0) 226 ? 'light' 227 : 'dark', 228 navigationBar: t.name !== 'light' ? 'light' : 'dark',