···23import {ThemeProvider} from '#/lib/ThemeContext'
24import I18nProvider from '#/locale/i18nProvider'
25import {logger} from '#/logger'
26-import {isAndroid, isIOS} from '#/platform/detection'
27import {Provider as A11yProvider} from '#/state/a11y'
28import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
29import {Provider as DialogStateProvider} from '#/state/dialogs'
···69import {ToastOutlet} from '#/components/Toast'
70import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
71import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
072import {
73 prefetchLiveEvents,
74 Provider as LiveEventsProvider,
···79import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
8081SplashScreen.preventAutoHideAsync()
82-if (isIOS) {
83 SystemUI.setBackgroundColorAsync('black')
84}
85-if (isAndroid) {
86 // iOS is handled by the config plugin -sfn
87 ScreenOrientation.lockAsync(
88 ScreenOrientation.OrientationLock.PORTRAIT_UP,
···23import {ThemeProvider} from '#/lib/ThemeContext'
24import I18nProvider from '#/locale/i18nProvider'
25import {logger} from '#/logger'
026import {Provider as A11yProvider} from '#/state/a11y'
27import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
28import {Provider as DialogStateProvider} from '#/state/dialogs'
···68import {ToastOutlet} from '#/components/Toast'
69import {Provider as AgeAssuranceV2Provider} from '#/ageAssurance'
70import {prefetchAgeAssuranceConfig} from '#/ageAssurance'
71+import {IS_ANDROID, IS_IOS} from '#/env'
72import {
73 prefetchLiveEvents,
74 Provider as LiveEventsProvider,
···79import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
8081SplashScreen.preventAutoHideAsync()
82+if (IS_IOS) {
83 SystemUI.setBackgroundColorAsync('black')
84}
85+if (IS_ANDROID) {
86 // iOS is handled by the config plugin -sfn
87 ScreenOrientation.lockAsync(
88 ScreenOrientation.OrientationLock.PORTRAIT_UP,
+6-6
src/Navigation.tsx
···44import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
45import {bskyTitle} from '#/lib/strings/headings'
46import {logger} from '#/logger'
47-import {isNative, isWeb} from '#/platform/detection'
48import {useDisableVerifyEmailReminder} from '#/state/preferences/disable-verify-email-reminder'
49import {useUnreadNotifications} from '#/state/queries/notifications/unread'
50import {useSession} from '#/state/session'
···140 EmailDialogScreenID,
141 useEmailDialogControl,
142} from '#/components/dialogs/EmailDialog'
0143import {router} from '#/routes'
144import {Referrer} from '../modules/expo-bluesky-swiss-army'
145···852 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
853 // since it will be created by react-navigation.
854 if (path.includes('intent/')) {
855- if (isNative) return
856 return buildStateObject('Flat', 'Home', params)
857 }
858859- if (isNative) {
860 if (name === 'Search') {
861 return buildStateObject('SearchTab', 'Search', params)
862 }
···933 )
934935 async function handlePushNotificationEntry() {
936- if (!isNative) return
937938 // deep links take precedence - on android,
939 // getLastNotificationResponseAsync returns a "notification"
···1085 navigationRef.dispatch(
1086 CommonActions.reset({
1087 index: 0,
1088- routes: [{name: isNative ? 'HomeTab' : 'Home'}],
1089 }),
1090 )
1091 return Promise.race([
···1119 initMs,
1120 })
11211122- if (isWeb) {
1123 const referrerInfo = Referrer.getReferrerInfo()
1124 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
1125 logEvent('deepLink:referrerReceived', {
···44import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
45import {bskyTitle} from '#/lib/strings/headings'
46import {logger} from '#/logger'
047import {useDisableVerifyEmailReminder} from '#/state/preferences/disable-verify-email-reminder'
48import {useUnreadNotifications} from '#/state/queries/notifications/unread'
49import {useSession} from '#/state/session'
···139 EmailDialogScreenID,
140 useEmailDialogControl,
141} from '#/components/dialogs/EmailDialog'
142+import {IS_NATIVE, IS_WEB} from '#/env'
143import {router} from '#/routes'
144import {Referrer} from '../modules/expo-bluesky-swiss-army'
145···852 // native, since the home tab and the home screen are defined as initial routes, we don't need to return a state
853 // since it will be created by react-navigation.
854 if (path.includes('intent/')) {
855+ if (IS_NATIVE) return
856 return buildStateObject('Flat', 'Home', params)
857 }
858859+ if (IS_NATIVE) {
860 if (name === 'Search') {
861 return buildStateObject('SearchTab', 'Search', params)
862 }
···933 )
934935 async function handlePushNotificationEntry() {
936+ if (!IS_NATIVE) return
937938 // deep links take precedence - on android,
939 // getLastNotificationResponseAsync returns a "notification"
···1085 navigationRef.dispatch(
1086 CommonActions.reset({
1087 index: 0,
1088+ routes: [{name: IS_NATIVE ? 'HomeTab' : 'Home'}],
1089 }),
1090 )
1091 return Promise.race([
···1119 initMs,
1120 })
11211122+ if (IS_WEB) {
1123 const referrerInfo = Referrer.getReferrerInfo()
1124 if (referrerInfo && referrerInfo.hostname !== 'bsky.app') {
1125 logEvent('deepLink:referrerReceived', {
+5-5
src/ageAssurance/components/NoAccessScreen.tsx
···10} from '#/lib/hooks/useCreateSupportLink'
11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
12import {logger} from '#/logger'
13-import {isWeb} from '#/platform/detection'
14-import {isNative} from '#/platform/detection'
15import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
16import {useSessionApi} from '#/state/session'
17import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
···38 isLegacyBirthdateBug,
39 useAgeAssuranceRegionConfig,
40} from '#/ageAssurance/util'
0041import {useDeviceGeolocationApi} from '#/geolocation'
4243const textStyles = [a.text_md, a.leading_snug]
···74 }, [])
7576 const onPressLogout = useCallback(() => {
77- if (isWeb) {
78 // We're switching accounts, which remounts the entire app.
79 // On mobile, this gets us Home, but on the web we also need reset the URL.
80 // We can't change the URL via a navigate() call because the navigator
···139 contentContainerStyle={[
140 a.px_2xl,
141 {
142- paddingTop: isWeb
143 ? a.p_5xl.padding
144 : insets.top + a.p_2xl.padding,
145 paddingBottom: 100,
···359 )}
360361 <View style={[a.gap_xs]}>
362- {isNative && (
363 <>
364 <Admonition>
365 <Trans>
···10} from '#/lib/hooks/useCreateSupportLink'
11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
12import {logger} from '#/logger'
0013import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
14import {useSessionApi} from '#/state/session'
15import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
···36 isLegacyBirthdateBug,
37 useAgeAssuranceRegionConfig,
38} from '#/ageAssurance/util'
39+import {IS_WEB} from '#/env'
40+import {IS_NATIVE} from '#/env'
41import {useDeviceGeolocationApi} from '#/geolocation'
4243const textStyles = [a.text_md, a.leading_snug]
···74 }, [])
7576 const onPressLogout = useCallback(() => {
77+ if (IS_WEB) {
78 // We're switching accounts, which remounts the entire app.
79 // On mobile, this gets us Home, but on the web we also need reset the URL.
80 // We can't change the URL via a navigate() call because the navigator
···139 contentContainerStyle={[
140 a.px_2xl,
141 {
142+ paddingTop: IS_WEB
143 ? a.p_5xl.padding
144 : insets.top + a.p_2xl.padding,
145 paddingBottom: 100,
···359 )}
360361 <View style={[a.gap_xs]}>
362+ {IS_NATIVE && (
363 <>
364 <Admonition>
365 <Trans>
+4-4
src/ageAssurance/components/RedirectOverlay.tsx
···15import {retry} from '#/lib/async/retry'
16import {wait} from '#/lib/async/wait'
17import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
18-import {isWeb} from '#/platform/detection'
19-import {isIOS} from '#/platform/detection'
20import {useAgent, useSession} from '#/state/session'
21import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
22import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···28import {Text} from '#/components/Typography'
29import {refetchAgeAssuranceServerState} from '#/ageAssurance'
30import {logger} from '#/ageAssurance'
003132export type RedirectOverlayState = {
33 result: 'success' | 'unknown'
···92 actorDid: params.get('actorDid') ?? undefined,
93 })
9495- if (isWeb) {
96 // Clear the URL parameters so they don't re-trigger
97 history.pushState(null, '', '/')
98 }
···149 // setting a zIndex when using FullWindowOverlay on iOS
150 // means the taps pass straight through to the underlying content (???)
151 // so don't set it on iOS. FullWindowOverlay already does the job.
152- !isIOS && {zIndex: 9999},
153 t.atoms.bg,
154 gtMobile ? a.p_2xl : a.p_xl,
155 a.align_center,
···15import {retry} from '#/lib/async/retry'
16import {wait} from '#/lib/async/wait'
17import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
0018import {useAgent, useSession} from '#/state/session'
19import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
20import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···26import {Text} from '#/components/Typography'
27import {refetchAgeAssuranceServerState} from '#/ageAssurance'
28import {logger} from '#/ageAssurance'
29+import {IS_WEB} from '#/env'
30+import {IS_IOS} from '#/env'
3132export type RedirectOverlayState = {
33 result: 'success' | 'unknown'
···92 actorDid: params.get('actorDid') ?? undefined,
93 })
9495+ if (IS_WEB) {
96 // Clear the URL parameters so they don't re-trigger
97 history.pushState(null, '', '/')
98 }
···149 // setting a zIndex when using FullWindowOverlay on iOS
150 // means the taps pass straight through to the underlying content (???)
151 // so don't set it on iOS. FullWindowOverlay already does the job.
152+ !IS_IOS && {zIndex: 9999},
153 t.atoms.bg,
154 gtMobile ? a.p_2xl : a.p_xl,
155 a.align_center,
+4-4
src/alf/fonts.ts
···1import {type TextStyle} from 'react-native'
23-import {isAndroid, isWeb} from '#/platform/detection'
4import {type Device, device} from '#/storage'
56const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
···39 */
40export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
41 if (fontFamily === 'theme') {
42- if (isAndroid) {
43 style.fontFamily =
44 {
45 400: 'Inter-Regular',
···71 }
72 }
7374- if (isWeb) {
75 // fallback families only supported on web
76 style.fontFamily += `, ${WEB_FONT_FAMILIES}`
77 }
···83 style.fontVariant = (style.fontVariant || []).concat('no-contextual')
84 } else {
85 // fallback families only supported on web
86- if (isWeb) {
87 style.fontFamily = style.fontFamily || WEB_FONT_FAMILIES
88 }
89
···1import {type TextStyle} from 'react-native'
23+import {IS_ANDROID, IS_WEB} from '#/env'
4import {type Device, device} from '#/storage'
56const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
···39 */
40export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') {
41 if (fontFamily === 'theme') {
42+ if (IS_ANDROID) {
43 style.fontFamily =
44 {
45 400: 'Inter-Regular',
···71 }
72 }
7374+ if (IS_WEB) {
75 // fallback families only supported on web
76 style.fontFamily += `, ${WEB_FONT_FAMILIES}`
77 }
···83 style.fontVariant = (style.fontVariant || []).concat('no-contextual')
84 } else {
85 // fallback families only supported on web
86+ if (IS_WEB) {
87 style.fontFamily = style.fontFamily || WEB_FONT_FAMILIES
88 }
89
+4-4
src/alf/typography.tsx
···4import {UITextView} from 'react-native-uitextview'
5import createEmojiRegex from 'emoji-regex'
67-import {isNative} from '#/platform/detection'
8-import {isIOS} from '#/platform/detection'
9import {type Alf, applyFonts, atoms, flatten} from '#/alf'
001011/**
12 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
···34 if (s.lineHeight !== 0 && s.lineHeight <= 2) {
35 s.lineHeight = Math.round(s.fontSize * s.lineHeight)
36 }
37- } else if (!isNative) {
38 s.lineHeight = s.fontSize
39 }
40···81 props: Omit<TextProps, 'children'> = {},
82 emoji: boolean,
83) {
84- if (!isIOS || !emoji) {
85 return children
86 }
87 return Children.map(children, child => {
···4import {UITextView} from 'react-native-uitextview'
5import createEmojiRegex from 'emoji-regex'
6007import {type Alf, applyFonts, atoms, flatten} from '#/alf'
8+import {IS_NATIVE} from '#/env'
9+import {IS_IOS} from '#/env'
1011/**
12 * Ensures that `lineHeight` defaults to a relative value of `1`, or applies
···34 if (s.lineHeight !== 0 && s.lineHeight <= 2) {
35 s.lineHeight = Math.round(s.fontSize * s.lineHeight)
36 }
37+ } else if (!IS_NATIVE) {
38 s.lineHeight = s.fontSize
39 }
40···81 props: Omit<TextProps, 'children'> = {},
82 emoji: boolean,
83) {
84+ if (!IS_IOS || !emoji) {
85 return children
86 }
87 return Children.map(children, child => {
+2-2
src/alf/util/systemUI.ts
···2import {type Theme} from '@bsky.app/alf'
34import {logger} from '#/logger'
5-import {isAndroid} from '#/platform/detection'
67export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
8- if (isAndroid) {
9 try {
10 if (themeType === 'theme') {
11 SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
···2import {type Theme} from '@bsky.app/alf'
34import {logger} from '#/logger'
5+import {IS_ANDROID} from '#/env'
67export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) {
8+ if (IS_ANDROID) {
9 try {
10 if (themeType === 'theme') {
11 SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor)
+2-2
src/alf/util/useColorModeTheme.ts
···2import {type ColorSchemeName, useColorScheme} from 'react-native'
3import {type ThemeName} from '@bsky.app/alf'
45-import {isWeb} from '#/platform/detection'
6import {useThemePrefs} from '#/state/shell'
7import {dark, dim, light} from '#/alf/themes'
089export function useColorModeTheme(): ThemeName {
10 const theme = useThemeName()
···4041function updateDocument(theme: ThemeName) {
42 // @ts-ignore web only
43- if (isWeb && typeof window !== 'undefined') {
44 // @ts-ignore web only
45 const html = window.document.documentElement
46 // @ts-ignore web only
···2import {type ColorSchemeName, useColorScheme} from 'react-native'
3import {type ThemeName} from '@bsky.app/alf'
405import {useThemePrefs} from '#/state/shell'
6import {dark, dim, light} from '#/alf/themes'
7+import {IS_WEB} from '#/env'
89export function useColorModeTheme(): ThemeName {
10 const theme = useThemeName()
···4041function updateDocument(theme: ThemeName) {
42 // @ts-ignore web only
43+ if (IS_WEB && typeof window !== 'undefined') {
44 // @ts-ignore web only
45 const html = window.document.documentElement
46 // @ts-ignore web only
+4-4
src/components/BlockedGeoOverlay.tsx
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
8-import {isWeb} from '#/platform/detection'
9-import {useDeviceGeolocationApi} from '#/state/geolocation'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···20import * as Toast from '#/components/Toast'
21import {Text} from '#/components/Typography'
22import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
02324export function BlockedGeoOverlay() {
25 const t = useTheme()
···70 contentContainerStyle={[
71 a.px_2xl,
72 {
73- paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
74 paddingBottom: 100,
75 },
76 ]}>
···117 ))}
118 </View>
119120- {!isWeb && (
121 <>
122 <View style={[a.pt_2xl]}>
123 <Divider />
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
8+import {useDeviceGeolocationApi} from '#/geolocation'
09import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
11import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···19import * as Toast from '#/components/Toast'
20import {Text} from '#/components/Typography'
21import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
22+import {IS_WEB} from '#/env'
2324export function BlockedGeoOverlay() {
25 const t = useTheme()
···70 contentContainerStyle={[
71 a.px_2xl,
72 {
73+ paddingTop: IS_WEB ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
74 paddingBottom: 100,
75 },
76 ]}>
···117 ))}
118 </View>
119120+ {!IS_WEB && (
121 <>
122 <View style={[a.pt_2xl]}>
123 <Divider />
···1import {useCallback} from 'react'
2import {SystemBars} from 'react-native-edge-to-edge'
34-import {isIOS} from '#/platform/detection'
56/**
7 * If we're calling a system API like the image picker that opens a sheet
···9 */
10export function useSheetWrapper() {
11 return useCallback(async <T>(promise: Promise<T>): Promise<T> => {
12- if (isIOS) {
13 const entry = SystemBars.pushStackEntry({
14 style: {
15 statusBar: 'light',
···1import {useCallback} from 'react'
2import {SystemBars} from 'react-native-edge-to-edge'
34+import {IS_IOS} from '#/env'
56/**
7 * If we're calling a system API like the image picker that opens a sheet
···9 */
10export function useSheetWrapper() {
11 return useCallback(async <T>(promise: Promise<T>): Promise<T> => {
12+ if (IS_IOS) {
13 const entry = SystemBars.pushStackEntry({
14 style: {
15 statusBar: 'light',
···11} from 'react-native-reanimated'
12import {useSafeAreaInsets} from 'react-native-safe-area-context'
1314-import {isWeb} from '#/platform/detection'
15import {useShellLayout} from '#/state/shell/shell-layout'
16import {
17 atoms as a,
···23import {useDialogContext} from '#/components/Dialog'
24import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
25import {ScrollbarOffsetContext} from '#/components/Layout/context'
02627export * from '#/components/Layout/const'
28export * 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'
12import {useSafeAreaInsets} from 'react-native-safe-area-context'
13014import {useShellLayout} from '#/state/shell/shell-layout'
15import {
16 atoms as a,
···22import {useDialogContext} from '#/components/Dialog'
23import {CENTER_COLUMN_OFFSET, SCROLLBAR_OFFSET} from '#/components/Layout/const'
24import {ScrollbarOffsetContext} from '#/components/Layout/context'
25+import {IS_WEB} from '#/env'
2627export * from '#/components/Layout/const'
28export * 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'
22import {useModalControls} from '#/state/modals'
23import {useGoLinksEnabled} from '#/state/preferences'
24import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf'
25import {Button, type ButtonProps} from '#/components/Button'
26import {useInteractionState} from '#/components/hooks/useInteractionState'
27import {Text, type TextProps} from '#/components/Typography'
028import {router} from '#/routes'
29import {useGlobalDialogsControlContext} from './dialogs/Context'
30···133 linkRequiresWarning(href, displayText),
134 )
135136- if (isWeb) {
137 e.preventDefault()
138 }
139···166 ]
167168 // does not apply to web's flat navigator
169- if (isNative && screen !== 'NotFound') {
170 const state = navigation.getState()
171 // if screen is not in the current navigator, it means it's
172 // most likely a tab screen. note: state can be undefined
···251 (e: GestureResponderEvent) => {
252 const exitEarlyIfFalse = outerOnLongPress?.(e)
253 if (exitEarlyIfFalse === false) return
254- return isNative && shareOnLongPress ? handleLongPress() : undefined
255 },
256 [outerOnLongPress, handleLongPress, shareOnLongPress],
257 )
···506 onPress,
507 ...props
508}: Omit<InlineLinkProps, 'onLongPress'>) {
509- return isWeb ? (
510 <InlineLinkText {...props} to={to} onPress={onPress}>
511 {children}
512 </InlineLinkText>
···552): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
553 return {
554 onPress(e: GestureResponderEvent) {
555- if (!isWeb || !isModifiedClickEvent(e)) {
556 e.preventDefault()
557 onPressHandler(e)
558 return false
···566 * intends to deviate from default behavior.
567 */
568export function isClickEventWithMetaKey(e: GestureResponderEvent) {
569- if (!isWeb) return false
570 const event = e as unknown as MouseEvent
571 return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
572}
···575 * Determines if the web click target is anything other than `_self`
576 */
577export function isClickTargetExternal(e: GestureResponderEvent) {
578- if (!isWeb) return false
579 const event = e as unknown as MouseEvent
580 const el = event.currentTarget as HTMLAnchorElement
581 return el && el.target && el.target !== '_self'
···587 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
588 */
589export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
590- if (!isWeb) return false
591 const event = e as unknown as MouseEvent
592 const isPrimaryButton = event.button === 0
593 return (
···601 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
602 */
603export function shouldClickOpenNewTab(e: GestureResponderEvent) {
604- if (!isWeb) return false
605 const event = e as unknown as MouseEvent
606- const isMiddleClick = isWeb && event.button === 1
607 return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
608}
···18 isExternalUrl,
19 linkRequiresWarning,
20} from '#/lib/strings/url-helpers'
021import {useModalControls} from '#/state/modals'
22import {useGoLinksEnabled} from '#/state/preferences'
23import {atoms as a, flatten, type TextStyleProp, useTheme, web} from '#/alf'
24import {Button, type ButtonProps} from '#/components/Button'
25import {useInteractionState} from '#/components/hooks/useInteractionState'
26import {Text, type TextProps} from '#/components/Typography'
27+import {IS_NATIVE, IS_WEB} from '#/env'
28import {router} from '#/routes'
29import {useGlobalDialogsControlContext} from './dialogs/Context'
30···133 linkRequiresWarning(href, displayText),
134 )
135136+ if (IS_WEB) {
137 e.preventDefault()
138 }
139···166 ]
167168 // does not apply to web's flat navigator
169+ if (IS_NATIVE && screen !== 'NotFound') {
170 const state = navigation.getState()
171 // if screen is not in the current navigator, it means it's
172 // most likely a tab screen. note: state can be undefined
···251 (e: GestureResponderEvent) => {
252 const exitEarlyIfFalse = outerOnLongPress?.(e)
253 if (exitEarlyIfFalse === false) return
254+ return IS_NATIVE && shareOnLongPress ? handleLongPress() : undefined
255 },
256 [outerOnLongPress, handleLongPress, shareOnLongPress],
257 )
···506 onPress,
507 ...props
508}: Omit<InlineLinkProps, 'onLongPress'>) {
509+ return IS_WEB ? (
510 <InlineLinkText {...props} to={to} onPress={onPress}>
511 {children}
512 </InlineLinkText>
···552): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} {
553 return {
554 onPress(e: GestureResponderEvent) {
555+ if (!IS_WEB || !isModifiedClickEvent(e)) {
556 e.preventDefault()
557 onPressHandler(e)
558 return false
···566 * intends to deviate from default behavior.
567 */
568export function isClickEventWithMetaKey(e: GestureResponderEvent) {
569+ if (!IS_WEB) return false
570 const event = e as unknown as MouseEvent
571 return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey
572}
···575 * Determines if the web click target is anything other than `_self`
576 */
577export function isClickTargetExternal(e: GestureResponderEvent) {
578+ if (!IS_WEB) return false
579 const event = e as unknown as MouseEvent
580 const el = event.currentTarget as HTMLAnchorElement
581 return el && el.target && el.target !== '_self'
···587 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
588 */
589export function isModifiedClickEvent(e: GestureResponderEvent): boolean {
590+ if (!IS_WEB) return false
591 const event = e as unknown as MouseEvent
592 const isPrimaryButton = event.button === 0
593 return (
···601 * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button}
602 */
603export function shouldClickOpenNewTab(e: GestureResponderEvent) {
604+ if (!IS_WEB) return false
605 const event = e as unknown as MouseEvent
606+ const isMiddleClick = IS_WEB && event.button === 1
607 return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick
608}
+2-2
src/components/MediaInsetBorder.tsx
···1import {StyleSheet} from 'react-native'
2import type React from 'react'
34-import {isHighDPI} from '#/lib/browser'
5import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf'
6import {Fill} from '#/components/Fill'
078/**
9 * Applies and thin border within a bounding box. Used to contrast media from
···33 // while we generally use hairlineWidth (aka 1px),
34 // we make an exception here for high DPI screens
35 // as the 1px border is very noticeable -sfn
36- web: isHighDPI ? 0.5 : StyleSheet.hairlineWidth,
37 }),
38 },
39 opaque
···1import {StyleSheet} from 'react-native'
2import type React from 'react'
304import {atoms as a, platform, useTheme, type ViewStyleProp} from '#/alf'
5import {Fill} from '#/components/Fill'
6+import {IS_HIGH_DPI} from '#/env'
78/**
9 * Applies and thin border within a bounding box. Used to contrast media from
···33 // while we generally use hairlineWidth (aka 1px),
34 // we make an exception here for high DPI screens
35 // as the 1px border is very noticeable -sfn
36+ web: IS_HIGH_DPI ? 0.5 : StyleSheet.hairlineWidth,
37 }),
38 },
39 opaque
+5-5
src/components/Menu/index.tsx
···10import {useLingui} from '@lingui/react'
11import flattenReactChildren from 'react-keyed-flatten-children'
1213-import {isAndroid, isIOS, isNative} from '#/platform/detection'
14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
15import {atoms as a, useTheme} from '#/alf'
16import {Button, ButtonText} from '#/components/Button'
···31 type TriggerProps,
32} from '#/components/Menu/types'
33import {Text} from '#/components/Typography'
03435export {
36 type DialogControlProps as MenuControlProps,
···71 } = useInteractionState()
7273 return children({
74- isNative: true,
75 control: context.control,
76 state: {
77 hovered: false,
···112 <Dialog.ScrollableInner label={_(msg`Menu`)}>
113 <View style={[a.gap_lg]}>
114 {children}
115- {isNative && showCancel && <Cancel />}
116 </View>
117 </Dialog.ScrollableInner>
118 </Context.Provider>
···138 onFocus={onFocus}
139 onBlur={onBlur}
140 onPress={async e => {
141- if (isAndroid) {
142 /**
143 * Below fix for iOS doesn't work for Android, this does.
144 */
145 onPress?.(e)
146 context.control.close()
147- } else if (isIOS) {
148 /**
149 * Fixes a subtle bug on iOS
150 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127}
···10import {useLingui} from '@lingui/react'
11import flattenReactChildren from 'react-keyed-flatten-children'
12013import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
14import {atoms as a, useTheme} from '#/alf'
15import {Button, ButtonText} from '#/components/Button'
···30 type TriggerProps,
31} from '#/components/Menu/types'
32import {Text} from '#/components/Typography'
33+import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env'
3435export {
36 type DialogControlProps as MenuControlProps,
···71 } = useInteractionState()
7273 return children({
74+ IS_NATIVE: true,
75 control: context.control,
76 state: {
77 hovered: false,
···112 <Dialog.ScrollableInner label={_(msg`Menu`)}>
113 <View style={[a.gap_lg]}>
114 {children}
115+ {IS_NATIVE && showCancel && <Cancel />}
116 </View>
117 </Dialog.ScrollableInner>
118 </Context.Provider>
···138 onFocus={onFocus}
139 onBlur={onBlur}
140 onPress={async e => {
141+ if (IS_ANDROID) {
142 /**
143 * Below fix for iOS doesn't work for Android, this does.
144 */
145 onPress?.(e)
146 context.control.close()
147+ } else if (IS_IOS) {
148 /**
149 * Fixes a subtle bug on iOS
150 * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127}
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
10import {sanitizeDisplayName} from '#/lib/strings/display-names'
11-import {isNative} from '#/platform/detection'
12import {useModerationOpts} from '#/state/preferences/moderation-opts'
13import {useSession} from '#/state/session'
14import {atoms as a, useTheme, web} from '#/alf'
···18import {Newskie} from '#/components/icons/Newskie'
19import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'
20import {Text} from '#/components/Typography'
02122export function NewskieDialog({
23 profile,
···162 </StarterPackCard.Link>
163 ) : null}
164165- {isNative && (
166 <Button
167 label={_(msg`Close`)}
168 color="secondary"
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
10import {sanitizeDisplayName} from '#/lib/strings/display-names'
011import {useModerationOpts} from '#/state/preferences/moderation-opts'
12import {useSession} from '#/state/session'
13import {atoms as a, useTheme, web} from '#/alf'
···17import {Newskie} from '#/components/icons/Newskie'
18import * as StarterPackCard from '#/components/StarterPack/StarterPackCard'
19import {Text} from '#/components/Typography'
20+import {IS_NATIVE} from '#/env'
2122export function NewskieDialog({
23 profile,
···162 </StarterPackCard.Link>
163 ) : null}
164165+ {IS_NATIVE && (
166 <Button
167 label={_(msg`Close`)}
168 color="secondary"
+3-3
src/components/PolicyUpdateOverlay/Overlay.tsx
···7import {LinearGradient} from 'expo-linear-gradient'
8import {utils} from '@bsky.app/alf'
910-import {isAndroid, isNative} from '#/platform/detection'
11import {useA11y} from '#/state/a11y'
12import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
13import {FocusScope} from '#/components/FocusScope'
14import {LockScroll} from '#/components/LockScroll'
01516const GUTTER = 24
17···80 a.z_20,
81 a.align_center,
82 !gtPhone && [a.justify_end, {minHeight: frame.height}],
83- isNative && [
84 {
85 paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
86 },
···109110 <FocusScope>
111 <View
112- accessible={isAndroid}
113 role="dialog"
114 aria-role="dialog"
115 aria-label={label}
···7import {LinearGradient} from 'expo-linear-gradient'
8import {utils} from '@bsky.app/alf'
9010import {useA11y} from '#/state/a11y'
11import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
12import {FocusScope} from '#/components/FocusScope'
13import {LockScroll} from '#/components/LockScroll'
14+import {IS_ANDROID, IS_NATIVE} from '#/env'
1516const GUTTER = 24
17···80 a.z_20,
81 a.align_center,
82 !gtPhone && [a.justify_end, {minHeight: frame.height}],
83+ IS_NATIVE && [
84 {
85 paddingBottom: Math.max(insets.bottom, a.p_2xl.padding),
86 },
···109110 <FocusScope>
111 <View
112+ accessible={IS_ANDROID}
113 role="dialog"
114 aria-role="dialog"
115 aria-label={label}
+2-2
src/components/PolicyUpdateOverlay/index.tsx
···1import {useEffect} from 'react'
2import {View} from 'react-native'
34-import {isIOS} from '#/platform/detection'
5import {atoms as a} from '#/alf'
6import {FullWindowOverlay} from '#/components/FullWindowOverlay'
7import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
8import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
9import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
01011export {Provider} from '#/components/PolicyUpdateOverlay/context'
12export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
···39 // setting a zIndex when using FullWindowOverlay on iOS
40 // means the taps pass straight through to the underlying content (???)
41 // so don't set it on iOS. FullWindowOverlay already does the job.
42- !isIOS && {zIndex: 9999},
43 ]}>
44 <Content state={state} />
45 </View>
···1import {useEffect} from 'react'
2import {View} from 'react-native'
304import {atoms as a} from '#/alf'
5import {FullWindowOverlay} from '#/components/FullWindowOverlay'
6import {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
7import {Portal} from '#/components/PolicyUpdateOverlay/Portal'
8import {Content} from '#/components/PolicyUpdateOverlay/updates/202508'
9+import {IS_IOS} from '#/env'
1011export {Provider} from '#/components/PolicyUpdateOverlay/context'
12export {usePolicyUpdateContext} from '#/components/PolicyUpdateOverlay/context'
···39 // setting a zIndex when using FullWindowOverlay on iOS
40 // means the taps pass straight through to the underlying content (???)
41 // so don't set it on iOS. FullWindowOverlay already does the job.
42+ !IS_IOS && {zIndex: 9999},
43 ]}>
44 <Content state={state} />
45 </View>
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isAndroid} from '#/platform/detection'
7import {useA11y} from '#/state/a11y'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonText} from '#/components/Button'
···12import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
13import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
14import {Text} from '#/components/Typography'
01516export function Content({state}: {state: PolicyUpdateState}) {
17 const t = useTheme()
···56 size: 'small',
57 } as const
5859- const label = isAndroid
60 ? _(
61 msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
62 )
···3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import {useA11y} from '#/state/a11y'
7import {atoms as a, useTheme} from '#/alf'
8import {Button, ButtonText} from '#/components/Button'
···11import {Overlay} from '#/components/PolicyUpdateOverlay/Overlay'
12import {type PolicyUpdateState} from '#/components/PolicyUpdateOverlay/usePolicyUpdateState'
13import {Text} from '#/components/Typography'
14+import {IS_ANDROID} from '#/env'
1516export function Content({state}: {state: PolicyUpdateState}) {
17 const t = useTheme()
···56 size: 'small',
57 } as const
5859+ const label = IS_ANDROID
60 ? _(
61 msg`We’re updating our Terms of Service, Privacy Policy, and Copyright Policy, effective September 15th, 2025. We're also updating our Community Guidelines, and we want your input! These new guidelines will take effect on October 15th, 2025. Learn more about these changes and how to share your thoughts with us by reading our blog post.`,
62 )
···26 type EmbedPlayerParams,
27 getPlayerAspect,
28} from '#/lib/strings/embed-player'
29-import {isNative} from '#/platform/detection'
30import {useExternalEmbedsPrefs} from '#/state/preferences'
31import {EventStopper} from '#/view/com/util/EventStopper'
32import {atoms as a, useTheme} from '#/alf'
···34import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
35import {Fill} from '#/components/Fill'
36import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
03738interface ShouldStartLoadRequest {
39 url: string
···148 const {height: winHeight, width: winWidth} = windowDims
149150 // Get the proper screen height depending on what is going on
151- const realWinHeight = isNative // If it is native, we always want the larger number
152 ? winHeight > winWidth
153 ? winHeight
154 : winWidth
···26 type EmbedPlayerParams,
27 getPlayerAspect,
28} from '#/lib/strings/embed-player'
029import {useExternalEmbedsPrefs} from '#/state/preferences'
30import {EventStopper} from '#/view/com/util/EventStopper'
31import {atoms as a, useTheme} from '#/alf'
···33import {EmbedConsentDialog} from '#/components/dialogs/EmbedConsent'
34import {Fill} from '#/components/Fill'
35import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
36+import {IS_NATIVE} from '#/env'
3738interface ShouldStartLoadRequest {
39 url: string
···148 const {height: winHeight, width: winWidth} = windowDims
149150 // Get the proper screen height depending on what is going on
151+ const realWinHeight = IS_NATIVE // If it is native, we always want the larger number
152 ? winHeight > winWidth
153 ? winHeight
154 : winWidth
+6-6
src/components/Post/Embed/ExternalEmbed/Gif.tsx
···13import {HITSLOP_20} from '#/lib/constants'
14import {clamp} from '#/lib/numbers'
15import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
16-import {isWeb} from '#/platform/detection'
17import {useAutoplayDisabled} from '#/state/preferences'
18import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
19import {atoms as a, useTheme} from '#/alf'
···22import * as Prompt from '#/components/Prompt'
23import {Text} from '#/components/Typography'
24import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
025import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
26import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
27···218 altContainer: {
219 backgroundColor: 'rgba(0, 0, 0, 0.75)',
220 borderRadius: 6,
221- paddingHorizontal: isWeb ? 8 : 6,
222- paddingVertical: isWeb ? 6 : 3,
223 position: 'absolute',
224 // Related to margin/gap hack. This keeps the alt label in the same position
225 // on all platforms
226- right: isWeb ? 8 : 5,
227- bottom: isWeb ? 8 : 5,
228 zIndex: 2,
229 },
230 alt: {
231 color: 'white',
232- fontSize: isWeb ? 10 : 7,
233 fontWeight: '600',
234 },
235})
···13import {HITSLOP_20} from '#/lib/constants'
14import {clamp} from '#/lib/numbers'
15import {type EmbedPlayerParams} from '#/lib/strings/embed-player'
016import {useAutoplayDisabled} from '#/state/preferences'
17import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
18import {atoms as a, useTheme} from '#/alf'
···21import * as Prompt from '#/components/Prompt'
22import {Text} from '#/components/Typography'
23import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
24+import {IS_WEB} from '#/env'
25import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
26import {type GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
27···218 altContainer: {
219 backgroundColor: 'rgba(0, 0, 0, 0.75)',
220 borderRadius: 6,
221+ paddingHorizontal: IS_WEB ? 8 : 6,
222+ paddingVertical: IS_WEB ? 6 : 3,
223 position: 'absolute',
224 // Related to margin/gap hack. This keeps the alt label in the same position
225 // on all platforms
226+ right: IS_WEB ? 8 : 5,
227+ bottom: IS_WEB ? 8 : 5,
228 zIndex: 2,
229 },
230 alt: {
231 color: 'white',
232+ fontSize: IS_WEB ? 10 : 7,
233 fontWeight: '600',
234 },
235})
+2-2
src/components/Post/Embed/ExternalEmbed/index.tsx
···10import {shareUrl} from '#/lib/sharing'
11import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
12import {toNiceDomain} from '#/lib/strings/url-helpers'
13-import {isNative} from '#/platform/detection'
14import {useExternalEmbedsPrefs} from '#/state/preferences'
15import {atoms as a, useTheme} from '#/alf'
16import {Divider} from '#/components/Divider'
17import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
18import {Link} from '#/components/Link'
19import {Text} from '#/components/Typography'
020import {ExternalGif} from './ExternalGif'
21import {ExternalPlayer} from './ExternalPlayer'
22import {GifEmbed} from './Gif'
···53 }, [playHaptic, onOpen])
5455 const onShareExternal = useCallback(() => {
56- if (link.uri && isNative) {
57 playHaptic('Heavy')
58 shareUrl(link.uri)
59 }
···10import {shareUrl} from '#/lib/sharing'
11import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
12import {toNiceDomain} from '#/lib/strings/url-helpers'
013import {useExternalEmbedsPrefs} from '#/state/preferences'
14import {atoms as a, useTheme} from '#/alf'
15import {Divider} from '#/components/Divider'
16import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
17import {Link} from '#/components/Link'
18import {Text} from '#/components/Typography'
19+import {IS_NATIVE} from '#/env'
20import {ExternalGif} from './ExternalGif'
21import {ExternalPlayer} from './ExternalPlayer'
22import {GifEmbed} from './Gif'
···53 }, [playHaptic, onOpen])
5455 const onShareExternal = useCallback(() => {
56+ if (link.uri && IS_NATIVE) {
57 playHaptic('Heavy')
58 shareUrl(link.uri)
59 }
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isFirefox, isTouchDevice} from '#/lib/browser'
7import {clamp} from '#/lib/numbers'
8import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
9import {atoms as a, useTheme, web} from '#/alf'
10import {useInteractionState} from '#/components/hooks/useInteractionState'
011import {formatTime} from './utils'
1213export function Scrubber({
···102 // a pointerUp event is fired outside the element that captured the
103 // pointer. Firefox clicks on the element the mouse is over, so we have
104 // to make everything unclickable while seeking -sfn
105- if (isFirefox && scrubberActive) {
106 document.body.classList.add('force-no-clicks')
107108 return () => {
···153 <View
154 testID="scrubber"
155 style={[
156- {height: isTouchDevice ? 32 : 18, width: '100%'},
157 a.flex_shrink_0,
158 a.px_xs,
159 ]}
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import {clamp} from '#/lib/numbers'
7import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
8import {atoms as a, useTheme, web} from '#/alf'
9import {useInteractionState} from '#/components/hooks/useInteractionState'
10+import {IS_WEB_FIREFOX, IS_WEB_TOUCH_DEVICE} from '#/env'
11import {formatTime} from './utils'
1213export function Scrubber({
···102 // a pointerUp event is fired outside the element that captured the
103 // pointer. Firefox clicks on the element the mouse is over, so we have
104 // to make everything unclickable while seeking -sfn
105+ if (IS_WEB_FIREFOX && scrubberActive) {
106 document.body.classList.add('force-no-clicks')
107108 return () => {
···153 <View
154 testID="scrubber"
155 style={[
156+ {height: IS_WEB_TOUCH_DEVICE ? 32 : 18, width: '100%'},
157 a.flex_shrink_0,
158 a.px_xs,
159 ]}
···4import {msg} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
67-import {isSafari, isTouchDevice} from '#/lib/browser'
8import {atoms as a} from '#/alf'
9import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
10import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
11import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
012import {ControlButton} from './ControlButton'
1314export function VolumeControl({
···57 onPointerEnter={onHover}
58 onPointerLeave={onEndHover}
59 style={[a.relative]}>
60- {hovered && !isTouchDevice && (
61 <Animated.View
62 entering={FadeIn.duration(100)}
63 exiting={FadeOut.duration(100)}
···80 aria-label={_(msg`Volume`)}
81 style={
82 // Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h
83- isSafari ? {height: 92, minHeight: '100%'} : {height: '100%'}
0084 }
85 onChange={onVolumeChange}
86 // @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn
···4import {msg} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
607import {atoms as a} from '#/alf'
8import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
9import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
10import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
11+import {IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE} from '#/env'
12import {ControlButton} from './ControlButton'
1314export function VolumeControl({
···57 onPointerEnter={onHover}
58 onPointerLeave={onEndHover}
59 style={[a.relative]}>
60+ {hovered && !IS_WEB_TOUCH_DEVICE && (
61 <Animated.View
62 entering={FadeIn.duration(100)}
63 exiting={FadeOut.duration(100)}
···80 aria-label={_(msg`Volume`)}
81 style={
82 // Ridiculous safari hack for old version of safari. Fixed in sonoma beta -h
83+ IS_WEB_SAFARI
84+ ? {height: 92, minHeight: '100%'}
85+ : {height: '100%'}
86 }
87 onChange={onVolumeChange}
88 // @ts-expect-error for old versions of firefox, and then re-using it for targeting the CSS -sfn
···1import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
23-import {isSafari} from '#/lib/browser'
4import {logger} from '#/logger'
5import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
067export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
8 const [playing, setPlaying] = useState(false)
···41 setCurrentTime(round(ref.current.currentTime) || 0)
42 // HACK: Safari randomly fires `stalled` events when changing between segments
43 // let's just clear the buffering state if the video is still progressing -sfn
44- if (isSafari) {
45 if (bufferingTimeout) clearTimeout(bufferingTimeout)
46 setBuffering(false)
47 }
···1import {type RefObject, useCallback, useEffect, useRef, useState} from 'react'
203import {logger} from '#/logger'
4import {useVideoVolumeState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
5+import {IS_WEB_SAFARI} from '#/env'
67export function useVideoElement(ref: RefObject<HTMLVideoElement | null>) {
8 const [playing, setPlaying] = useState(false)
···41 setCurrentTime(round(ref.current.currentTime) || 0)
42 // HACK: Safari randomly fires `stalled` events when changing between segments
43 // let's just clear the buffering state if the video is still progressing -sfn
44+ if (IS_WEB_SAFARI) {
45 if (bufferingTimeout) clearTimeout(bufferingTimeout)
46 setBuffering(false)
47 }
···1import {View} from 'react-native'
23-import {isTouchDevice} from '#/lib/browser'
4-import {isNative, isWeb} from '#/platform/detection'
5import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
067export function SubtleHover({
8 style,
···39 />
40 )
4142- if (isWeb && web) {
43- return isTouchDevice ? null : el
44- } else if (isNative && native) {
45 return el
46 }
47
···1import {View} from 'react-native'
2003import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
4+import {IS_NATIVE, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
56export function SubtleHover({
7 style,
···38 />
39 )
4041+ if (IS_WEB && web) {
42+ return IS_WEB_TOUCH_DEVICE ? null : el
43+ } else if (IS_NATIVE && native) {
44 return el
45 }
46
+1
src/components/Typography.tsx
···35 if (__DEV__) {
36 if (!emoji && childHasEmoji(children)) {
37 logger.warn(
038 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
39 )
40 }
···35 if (__DEV__) {
36 if (!emoji && childHasEmoji(children)) {
37 logger.warn(
38+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
39 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
40 )
41 }
+3-3
src/components/WhoCanReply.tsx
···18import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
20import {logger} from '#/logger'
21-import {isNative} from '#/platform/detection'
22import {
23 type ThreadgateAllowUISetting,
24 threadgateViewToAllowUISetting,
···37import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
38import {InlineLinkText} from '#/components/Link'
39import {Text} from '#/components/Typography'
040import * as bsky from '#/types/bsky'
4142interface WhoCanReplyProps {
···86 : _(msg`Some people can reply`)
8788 const onPressOpen = () => {
89- if (isNative && Keyboard.isVisible()) {
90 Keyboard.dismiss()
91 }
92 if (isThreadAuthor) {
···229 embeddingDisabled={embeddingDisabled}
230 />
231 </View>
232- {isNative && (
233 <Button
234 label={_(msg`Close`)}
235 onPress={() => control.close()}
···18import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
20import {logger} from '#/logger'
021import {
22 type ThreadgateAllowUISetting,
23 threadgateViewToAllowUISetting,
···36import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
37import {InlineLinkText} from '#/components/Link'
38import {Text} from '#/components/Typography'
39+import {IS_NATIVE} from '#/env'
40import * as bsky from '#/types/bsky'
4142interface WhoCanReplyProps {
···86 : _(msg`Some people can reply`)
8788 const onPressOpen = () => {
89+ if (IS_NATIVE && Keyboard.isVisible()) {
90 Keyboard.dismiss()
91 }
92 if (isThreadAuthor) {
···229 embeddingDisabled={embeddingDisabled}
230 />
231 </View>
232+ {IS_NATIVE && (
233 <Button
234 label={_(msg`Close`)}
235 onPress={() => control.close()}
···18import {cleanError} from '#/lib/strings/errors'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
21-import {isWeb} from '#/platform/detection'
22import {updateProfileShadow} from '#/state/cache/profile-shadow'
23import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
24import {useAgent} from '#/state/session'
···37import {Loader} from '#/components/Loader'
38import * as ProfileCard from '#/components/ProfileCard'
39import {Text} from '#/components/Typography'
040import type * as bsky from '#/types/bsky'
4142export function SubscribeProfileDialog({
···195 }
196 } else {
197 // on web, a disabled save button feels more natural than a massive close button
198- if (isWeb) {
199 return {
200 label: _(msg`Save changes`),
201 color: 'secondary',
···18import {cleanError} from '#/lib/strings/errors'
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
021import {updateProfileShadow} from '#/state/cache/profile-shadow'
22import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions'
23import {useAgent} from '#/state/session'
···36import {Loader} from '#/components/Loader'
37import * as ProfileCard from '#/components/ProfileCard'
38import {Text} from '#/components/Typography'
39+import {IS_WEB} from '#/env'
40import type * as bsky from '#/types/bsky'
4142export function SubscribeProfileDialog({
···195 }
196 } else {
197 // on web, a disabled save button feels more natural than a massive close button
198+ if (IS_WEB) {
199 return {
200 label: _(msg`Save changes`),
201 color: 'secondary',
···56import {retry} from '#/lib/async/retry'
7import {wait} from '#/lib/async/wait'
8-import {isNative} from '#/platform/detection'
9import {useAgent} from '#/state/session'
10import {atoms as a, useTheme, web} from '#/alf'
11import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···18import {Text} from '#/components/Typography'
19import {refetchAgeAssuranceServerState} from '#/ageAssurance'
20import {logger} from '#/ageAssurance'
02122export type AgeAssuranceRedirectDialogState = {
23 result: 'success' | 'unknown'
···166 </Trans>
167 </Text>
168169- {isNative && (
170 <View style={[a.w_full, a.pt_lg]}>
171 <Button
172 label={_(msg`Close`)}
···225 )}
226 </Text>
227228- {error && isNative && (
229 <View style={[a.w_full, a.pt_lg]}>
230 <Button
231 label={_(msg`Close`)}
···56import {retry} from '#/lib/async/retry'
7import {wait} from '#/lib/async/wait'
08import {useAgent} from '#/state/session'
9import {atoms as a, useTheme, web} from '#/alf'
10import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
···17import {Text} from '#/components/Typography'
18import {refetchAgeAssuranceServerState} from '#/ageAssurance'
19import {logger} from '#/ageAssurance'
20+import {IS_NATIVE} from '#/env'
2122export type AgeAssuranceRedirectDialogState = {
23 result: 'success' | 'unknown'
···166 </Trans>
167 </Text>
168169+ {IS_NATIVE && (
170 <View style={[a.w_full, a.pt_lg]}>
171 <Button
172 label={_(msg`Close`)}
···225 )}
226 </Text>
227228+ {error && IS_NATIVE && (
229 <View style={[a.w_full, a.pt_lg]}>
230 <Button
231 label={_(msg`Close`)}
+2-2
src/components/contacts/FindContactsBannerNUX.tsx
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGate} from '#/lib/statsig/statsig'
10import {logger} from '#/logger'
11-import {isWeb} from '#/platform/detection'
12import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
13import {atoms as a, useTheme} from '#/alf'
14import {Button} from '#/components/Button'
15import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
16import {Text} from '#/components/Typography'
017import {Link} from '../Link'
18import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
19···92 const gate = useGate()
9394 const visible = useMemo(() => {
95- if (isWeb) return false
96 if (hidden) return false
97 if (nux && nux.completed) return false
98 if (!isFeatureEnabled) return false
···8import {HITSLOP_10} from '#/lib/constants'
9import {useGate} from '#/lib/statsig/statsig'
10import {logger} from '#/logger'
011import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs'
12import {atoms as a, useTheme} from '#/alf'
13import {Button} from '#/components/Button'
14import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
15import {Text} from '#/components/Typography'
16+import {IS_WEB} from '#/env'
17import {Link} from '../Link'
18import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from './country-allowlist'
19···92 const gate = useGate()
9394 const visible = useMemo(() => {
95+ if (IS_WEB) return false
96 if (hidden) return false
97 if (nux && nux.completed) return false
98 if (!isFeatureEnabled) return false
+3-3
src/components/contacts/components/OTPInput.tsx
···9import {useLingui} from '@lingui/react'
1011import {mergeRefs} from '#/lib/merge-refs'
12-import {isAndroid, isIOS} from '#/platform/detection'
13import {atoms as a, ios, platform, useTheme} from '#/alf'
14import {useInteractionState} from '#/components/hooks/useInteractionState'
15import {Text} from '#/components/Typography'
01617export function OTPInput({
18 label,
···95 <TextInput
96 // SMS autofill is borked on iOS if you open the keyboard immediately -sfn
97 onLayout={ios(() => setTimeout(() => innerRef.current?.focus(), 100))}
98- autoFocus={isAndroid}
99 accessible
100 accessibilityLabel={label}
101 accessibilityHint=""
···135 android: {opacity: 0},
136 }),
137 ]}
138- caretHidden={isIOS}
139 clearTextOnFocus
140 />
141 </Pressable>
···9import {useLingui} from '@lingui/react'
1011import {mergeRefs} from '#/lib/merge-refs'
012import {atoms as a, ios, platform, useTheme} from '#/alf'
13import {useInteractionState} from '#/components/hooks/useInteractionState'
14import {Text} from '#/components/Typography'
15+import {IS_ANDROID, IS_IOS} from '#/env'
1617export function OTPInput({
18 label,
···95 <TextInput
96 // SMS autofill is borked on iOS if you open the keyboard immediately -sfn
97 onLayout={ios(() => setTimeout(() => innerRef.current?.focus(), 100))}
98+ autoFocus={IS_ANDROID}
99 accessible
100 accessibilityLabel={label}
101 accessibilityHint=""
···135 android: {opacity: 0},
136 }),
137 ]}
138+ caretHidden={IS_IOS}
139 clearTextOnFocus
140 />
141 </Pressable>
···6import {wait} from '#/lib/async/wait'
7import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError'
8import {logger} from '#/logger'
9-import {isWeb} from '#/platform/detection'
10import {atoms as a, useTheme, web} from '#/alf'
11import {Admonition} from '#/components/Admonition'
12import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···14import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
15import {Loader} from '#/components/Loader'
16import {Text} from '#/components/Typography'
017import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation'
1819export type Props = {
···138 disabled={isRequesting}
139 label={_(msg`Allow location access`)}
140 onPress={onPressConfirm}
141- size={isWeb ? 'small' : 'large'}
142 color="primary">
143 <ButtonIcon icon={isRequesting ? Loader : LocationIcon} />
144 <ButtonText>
···147 </Button>
148 )}
149150- {!isWeb && (
151 <Button
152 label={_(msg`Cancel`)}
153 onPress={() => close()}
154- size={isWeb ? 'small' : 'large'}
155 color="secondary">
156 <ButtonText>
157 <Trans>Cancel</Trans>
···6import {wait} from '#/lib/async/wait'
7import {isNetworkError, useCleanError} from '#/lib/hooks/useCleanError'
8import {logger} from '#/logger'
09import {atoms as a, useTheme, web} from '#/alf'
10import {Admonition} from '#/components/Admonition'
11import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···13import {PinLocation_Stroke2_Corner0_Rounded as LocationIcon} from '#/components/icons/PinLocation'
14import {Loader} from '#/components/Loader'
15import {Text} from '#/components/Typography'
16+import {IS_WEB} from '#/env'
17import {type Geolocation, useRequestDeviceGeolocation} from '#/geolocation'
1819export type Props = {
···138 disabled={isRequesting}
139 label={_(msg`Allow location access`)}
140 onPress={onPressConfirm}
141+ size={IS_WEB ? 'small' : 'large'}
142 color="primary">
143 <ButtonIcon icon={isRequesting ? Loader : LocationIcon} />
144 <ButtonText>
···147 </Button>
148 )}
149150+ {!IS_WEB && (
151 <Button
152 label={_(msg`Cancel`)}
153 onPress={() => close()}
154+ size={IS_WEB ? 'small' : 'large'}
155 color="secondary">
156 <ButtonText>
157 <Trans>Cancel</Trans>
+3-3
src/components/dialogs/GifSelect.tsx
···1314import {logEvent} from '#/lib/statsig/statsig'
15import {cleanError} from '#/lib/strings/errors'
16-import {isWeb} from '#/platform/detection'
17import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
18import {
19 type Gif,
···32import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
33import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
34import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
03536export function GifSelectDialog({
37 controlRef,
···152 a.pb_sm,
153 t.atoms.bg,
154 ]}>
155- {!gtMobile && isWeb && (
156 <Button
157 size="small"
158 variant="ghost"
···164 </Button>
165 )}
166167- <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}>
168 <TextField.Icon icon={Search} />
169 <TextField.Input
170 label={_(msg`Search GIFs`)}
···1314import {logEvent} from '#/lib/statsig/statsig'
15import {cleanError} from '#/lib/strings/errors'
016import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
17import {
18 type Gif,
···31import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
32import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
33import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
34+import {IS_WEB} from '#/env'
3536export function GifSelectDialog({
37 controlRef,
···152 a.pb_sm,
153 t.atoms.bg,
154 ]}>
155+ {!gtMobile && IS_WEB && (
156 <Button
157 size="small"
158 variant="ghost"
···164 </Button>
165 )}
166167+ <TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
168 <TextField.Icon icon={Search} />
169 <TextField.Input
170 label={_(msg`Search GIFs`)}
+2-2
src/components/dialogs/InAppBrowserConsent.tsx
···4import {useLingui} from '@lingui/react'
56import {useOpenLink} from '#/lib/hooks/useOpenLink'
7-import {isWeb} from '#/platform/detection'
8import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
9import {atoms as a, useTheme} from '#/alf'
10import {Button, ButtonIcon, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
12import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight'
13import {Text} from '#/components/Typography'
014import {useGlobalDialogsControlContext} from './Context'
1516export function InAppBrowserConsentDialog() {
17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
1819- if (isWeb) return null
2021 return (
22 <Dialog.Outer
···4import {useLingui} from '@lingui/react'
56import {useOpenLink} from '#/lib/hooks/useOpenLink'
07import {useSetInAppBrowser} from '#/state/preferences/in-app-browser'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonIcon, ButtonText} from '#/components/Button'
10import * as Dialog from '#/components/Dialog'
11import {SquareArrowTopRight_Stroke2_Corner0_Rounded as External} from '#/components/icons/SquareArrowTopRight'
12import {Text} from '#/components/Typography'
13+import {IS_WEB} from '#/env'
14import {useGlobalDialogsControlContext} from './Context'
1516export function InAppBrowserConsentDialog() {
17 const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
1819+ if (IS_WEB) return null
2021 return (
22 <Dialog.Outer
+2-2
src/components/dialogs/MutedWords.tsx
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
8-import {isNative} from '#/platform/detection'
9import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {
11 usePreferencesQuery,
···33import {Loader} from '#/components/Loader'
34import * as Prompt from '#/components/Prompt'
35import {Text} from '#/components/Typography'
03637const ONE_DAY = 24 * 60 * 60 * 1000
38···407 )}
408 </View>
409410- {isNative && <View style={{height: 20}} />}
411 </View>
412413 <Dialog.Close />
···5import {useLingui} from '@lingui/react'
67import {logger} from '#/logger'
08import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
9import {
10 usePreferencesQuery,
···32import {Loader} from '#/components/Loader'
33import * as Prompt from '#/components/Prompt'
34import {Text} from '#/components/Typography'
35+import {IS_NATIVE} from '#/env'
3637const ONE_DAY = 24 * 60 * 60 * 1000
38···407 )}
408 </View>
409410+ {IS_NATIVE && <View style={{height: 20}} />}
411 </View>
412413 <Dialog.Close />
···67import {urls} from '#/lib/constants'
8import {logger} from '#/logger'
9-import {isNative} from '#/platform/detection'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {atoms as a, useBreakpoints, useTheme} from '#/alf'
12import {Button, ButtonText} from '#/components/Button'
···16import {VerifierCheck} from '#/components/icons/VerifierCheck'
17import {Link} from '#/components/Link'
18import {Span, Text} from '#/components/Typography'
01920export function InitialVerificationAnnouncement() {
21 const t = useTheme()
···176 <Trans>Read blog post</Trans>
177 </ButtonText>
178 </Link>
179- {isNative && (
180 <Button
181 label={_(msg`Close`)}
182 size="small"
···67import {urls} from '#/lib/constants'
8import {logger} from '#/logger'
09import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {atoms as a, useBreakpoints, useTheme} from '#/alf'
11import {Button, ButtonText} from '#/components/Button'
···15import {VerifierCheck} from '#/components/icons/VerifierCheck'
16import {Link} from '#/components/Link'
17import {Span, Text} from '#/components/Typography'
18+import {IS_NATIVE} from '#/env'
1920export function InitialVerificationAnnouncement() {
21 const t = useTheme()
···176 <Trans>Read blog post</Trans>
177 </ButtonText>
178 </Link>
179+ {IS_NATIVE && (
180 <Button
181 label={_(msg`Close`)}
182 size="small"
···4import {useLingui} from '@lingui/react'
56import {HITSLOP_10} from '#/lib/constants'
7-import {isNative} from '#/platform/detection'
8import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
9import {atoms as a, useTheme} from '#/alf'
10import {Button, ButtonIcon} from '#/components/Button'
11import * as TextField from '#/components/forms/TextField'
12import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
13import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
01415type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
16 label?: TextField.InputProps['label']
···39 placeholder={_(msg`Search`)}
40 returnKeyType="search"
41 keyboardAppearance={t.scheme}
42- selectTextOnFocus={isNative}
43 autoFocus={false}
44 accessibilityRole="search"
45 autoCorrect={false}
···4import {useLingui} from '@lingui/react'
56import {HITSLOP_10} from '#/lib/constants'
07import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
8import {atoms as a, useTheme} from '#/alf'
9import {Button, ButtonIcon} from '#/components/Button'
10import * as TextField from '#/components/forms/TextField'
11import {MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon} from '#/components/icons/MagnifyingGlass'
12import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
13+import {IS_NATIVE} from '#/env'
1415type SearchInputProps = Omit<TextField.InputProps, 'label'> & {
16 label?: TextField.InputProps['label']
···39 placeholder={_(msg`Search`)}
40 returnKeyType="search"
41 keyboardAppearance={t.scheme}
42+ selectTextOnFocus={IS_NATIVE}
43 autoFocus={false}
44 accessibilityRole="search"
45 autoCorrect={false}
+2-2
src/components/forms/TextField.tsx
···1112import {HITSLOP_20} from '#/lib/constants'
13import {mergeRefs} from '#/lib/merge-refs'
14-import {isWeb} from '#/platform/detection'
15import {
16 android,
17 applyFonts,
···26import {useInteractionState} from '#/components/hooks/useInteractionState'
27import {type Props as SVGIconProps} from '#/components/icons/common'
28import {Text} from '#/components/Typography'
02930const Context = createContext<{
31 inputRef: React.RefObject<TextInput | null> | null
···106 a.align_center,
107 a.relative,
108 a.w_full,
109- !(hasMultiline && isWeb) && a.px_md,
110 style,
111 ]}
112 {...web({
···1112import {HITSLOP_20} from '#/lib/constants'
13import {mergeRefs} from '#/lib/merge-refs'
014import {
15 android,
16 applyFonts,
···25import {useInteractionState} from '#/components/hooks/useInteractionState'
26import {type Props as SVGIconProps} from '#/components/icons/common'
27import {Text} from '#/components/Typography'
28+import {IS_WEB} from '#/env/index.web'
2930const Context = createContext<{
31 inputRef: React.RefObject<TextInput | null> | null
···106 a.align_center,
107 a.relative,
108 a.w_full,
109+ !(hasMultiline && IS_WEB) && a.px_md,
110 style,
111 ]}
112 {...web({
+2-2
src/components/forms/Toggle/index.tsx
···1011import {HITSLOP_10} from '#/lib/constants'
12import {useHaptics} from '#/lib/haptics'
13-import {isNative} from '#/platform/detection'
14import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
15import {
16 atoms as a,
···23import {useInteractionState} from '#/components/hooks/useInteractionState'
24import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
25import {Text} from '#/components/Typography'
02627export * from './Panel'
28···564 )
565}
566567-export const Platform = isNative ? Switch : Checkbox
···1011import {HITSLOP_10} from '#/lib/constants'
12import {useHaptics} from '#/lib/haptics'
013import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
14import {
15 atoms as a,
···22import {useInteractionState} from '#/components/hooks/useInteractionState'
23import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
24import {Text} from '#/components/Typography'
25+import {IS_NATIVE} from '#/env'
2627export * from './Panel'
28···564 )
565}
566567+export const Platform = IS_NATIVE ? Switch : Checkbox
+3-4
src/components/hooks/useFullscreen.ts
···6 useSyncExternalStore,
7} from 'react'
89-import {isFirefox, isSafari} from '#/lib/browser'
10-import {isWeb} from '#/platform/detection'
1112function fullscreenSubscribe(onChange: () => void) {
13 document.addEventListener('fullscreenchange', onChange)
···15}
1617export function useFullscreen(ref?: React.RefObject<HTMLElement | null>) {
18- if (!isWeb) throw new Error("'useFullscreen' is a web-only hook")
19 const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
20 Boolean(document.fullscreenElement),
21 )
···3940 // Chrome has an issue where it doesn't scroll back to the top after exiting fullscreen
41 // Let's play it safe and do it if not FF or Safari, since anything else will probably be chromium
42- if (prevIsFullscreen && !isFirefox && !isSafari) {
43 setTimeout(() => {
44 if (scrollYRef.current !== null) {
45 window.scrollTo(0, scrollYRef.current)
···6 useSyncExternalStore,
7} from 'react'
89+import {IS_WEB, IS_WEB_FIREFOX, IS_WEB_SAFARI} from '#/env'
01011function fullscreenSubscribe(onChange: () => void) {
12 document.addEventListener('fullscreenchange', onChange)
···14}
1516export function useFullscreen(ref?: React.RefObject<HTMLElement | null>) {
17+ if (!IS_WEB) throw new Error("'useFullscreen' is a web-only hook")
18 const isFullscreen = useSyncExternalStore(fullscreenSubscribe, () =>
19 Boolean(document.fullscreenElement),
20 )
···3839 // Chrome has an issue where it doesn't scroll back to the top after exiting fullscreen
40 // Let's play it safe and do it if not FF or Safari, since anything else will probably be chromium
41+ if (prevIsFullscreen && !IS_WEB_FIREFOX && !IS_WEB_SAFARI) {
42 setTimeout(() => {
43 if (scrollYRef.current !== null) {
44 window.scrollTo(0, scrollYRef.current)
···4 createStarterPackLinkFromAndroidReferrer,
5 httpStarterPackUriToAtUri,
6} from '#/lib/strings/starter-pack'
7-import {isAndroid} from '#/platform/detection'
8import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
9import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
010import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
1112export function useStarterPackEntry() {
···32 ;(async () => {
33 let uri: string | null | undefined
3435- if (isAndroid) {
36 const res = await Referrer.getGooglePlayReferrerInfoAsync()
3738 if (res && res.installReferrer) {
···4 createStarterPackLinkFromAndroidReferrer,
5 httpStarterPackUriToAtUri,
6} from '#/lib/strings/starter-pack'
07import {useHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
8import {useSetActiveStarterPack} from '#/state/shell/starter-pack'
9+import {IS_ANDROID} from '#/env'
10import {Referrer, SharedPrefs} from '../../../modules/expo-bluesky-swiss-army'
1112export function useStarterPackEntry() {
···32 ;(async () => {
33 let uri: string | null | undefined
3435+ if (IS_ANDROID) {
36 const res = await Referrer.getGooglePlayReferrerInfoAsync()
3738 if (res && res.installReferrer) {
+2-2
src/components/hooks/useWelcomeModal.ts
···1import {useEffect, useState} from 'react'
23-import {isWeb} from '#/platform/detection'
4import {useSession} from '#/state/session'
056export function useWelcomeModal() {
7 const {hasSession} = useSession()
···22 // 2. We're on the web (this is a web-only feature)
23 // 3. We're on the homepage (path is '/' or '/home')
24 // 4. User hasn't actively closed the modal in this session
25- if (isWeb && !hasSession && typeof window !== 'undefined') {
26 const currentPath = window.location.pathname
27 const isHomePage = currentPath === '/'
28 const hasUserClosedModal =
···1import {useEffect, useState} from 'react'
203import {useSession} from '#/state/session'
4+import {IS_WEB} from '#/env'
56export function useWelcomeModal() {
7 const {hasSession} = useSession()
···22 // 2. We're on the web (this is a web-only feature)
23 // 3. We're on the homepage (path is '/' or '/home')
24 // 4. User hasn't actively closed the modal in this session
25+ if (IS_WEB && !hasSession && typeof window !== 'undefined') {
26 const currentPath = window.location.pathname
27 const isHomePage = currentPath === '/'
28 const hasUserClosedModal =
+2-2
src/components/images/AutoSizedImage.tsx
···11import {useLingui} from '@lingui/react'
1213import {type Dimensions} from '#/lib/media/types'
14-import {isNative} from '#/platform/detection'
15import {
16 maybeModifyHighQualityImage,
17 useHighQualityImages,
···21import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
22import {MediaInsetBorder} from '#/components/MediaInsetBorder'
23import {Text} from '#/components/Typography'
02425export function ConstrainedImage({
26 aspectRatio,
···39 * the height of the image.
40 */
41 const outerAspectRatio = useMemo<DimensionValue>(() => {
42- const ratio = isNative
43 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
44 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box
45 return `${ratio * 100}%`
···11import {useLingui} from '@lingui/react'
1213import {type Dimensions} from '#/lib/media/types'
014import {
15 maybeModifyHighQualityImage,
16 useHighQualityImages,
···20import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal'
21import {MediaInsetBorder} from '#/components/MediaInsetBorder'
22import {Text} from '#/components/Typography'
23+import {IS_NATIVE} from '#/env'
2425export function ConstrainedImage({
26 aspectRatio,
···39 * the height of the image.
40 */
41 const outerAspectRatio = useMemo<DimensionValue>(() => {
42+ const ratio = IS_NATIVE
43 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box
44 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box
45 return `${ratio * 100}%`
···13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
15import * as TextField from '#/components/forms/TextField'
16-import {getLiveServiceNames} from '#/components/live/utils'
000017import {Loader} from '#/components/Loader'
18import * as ProfileCard from '#/components/ProfileCard'
19import * as Select from '#/components/Select'
···21import type * as bsky from '#/types/bsky'
22import {LinkPreview} from './LinkPreview'
23import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
24-import {displayDuration, useDebouncedValue} from './utils'
2526export function GoLiveDialog({
27 control,
···5758 const time = useCallback(
59 (offset: number) => {
60- tick!
6162 const date = new Date()
63 date.setMinutes(date.getMinutes() + offset)
···13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
15import * as TextField from '#/components/forms/TextField'
16+import {
17+ displayDuration,
18+ getLiveServiceNames,
19+ useDebouncedValue,
20+} from '#/components/live/utils'
21import {Loader} from '#/components/Loader'
22import * as ProfileCard from '#/components/ProfileCard'
23import * as Select from '#/components/Select'
···25import type * as bsky from '#/types/bsky'
26import {LinkPreview} from './LinkPreview'
27import {useLiveLinkMetaQuery, useUpsertLiveStatusMutation} from './queries'
02829export function GoLiveDialog({
30 control,
···6061 const time = useCallback(
62 (offset: number) => {
63+ void tick
6465 const date = new Date()
66 date.setMinutes(date.getMinutes() + offset)
+2-2
src/components/moderation/LabelsOnMeDialog.tsx
···12import {makeProfileLink} from '#/lib/routes/links'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {logger} from '#/logger'
15-import {isAndroid} from '#/platform/detection'
16import {useAgent, useSession} from '#/state/session'
17import * as Toast from '#/view/com/util/Toast'
18import {atoms as a, useBreakpoints, useTheme} from '#/alf'
···20import * as Dialog from '#/components/Dialog'
21import {InlineLinkText} from '#/components/Link'
22import {Text} from '#/components/Typography'
023import {Admonition} from '../Admonition'
24import {Divider} from '../Divider'
25import {Loader} from '../Loader'
···344 {isPending && <ButtonIcon icon={Loader} />}
345 </Button>
346 </View>
347- {isAndroid && <View style={{height: 300}} />}
348 </>
349 )
350}
···12import {makeProfileLink} from '#/lib/routes/links'
13import {sanitizeHandle} from '#/lib/strings/handles'
14import {logger} from '#/logger'
015import {useAgent, useSession} from '#/state/session'
16import * as Toast from '#/view/com/util/Toast'
17import {atoms as a, useBreakpoints, useTheme} from '#/alf'
···19import * as Dialog from '#/components/Dialog'
20import {InlineLinkText} from '#/components/Link'
21import {Text} from '#/components/Typography'
22+import {IS_ANDROID} from '#/env'
23import {Admonition} from '../Admonition'
24import {Divider} from '../Divider'
25import {Loader} from '../Loader'
···344 {isPending && <ButtonIcon icon={Loader} />}
345 </Button>
346 </View>
347+ {IS_ANDROID && <View style={{height: 300}} />}
348 </>
349 )
350}
···13 * The short commit hash and environment of the current bundle.
14 */
15export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
0000000000000000000000000000000000
···3import {useLingui} from '@lingui/react'
45import {useCleanError} from '#/lib/hooks/useCleanError'
6-import {isNative} from '#/platform/detection'
7import {atoms as a, web} from '#/alf'
8import {Admonition} from '#/components/Admonition'
9import {Button, ButtonIcon, ButtonText} from '#/components/Button'
10import * as Dialog from '#/components/Dialog'
11import {Loader} from '#/components/Loader'
12-import * as toast from '#/components/Toast'
13import {Span, Text} from '#/components/Typography'
014import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
15import {
16 type LiveEventFeed,
···61 feed,
62 metricContext,
63 onUpdateSuccess({undoAction}) {
64- toast.show(
65- <toast.Outer>
66- <toast.Icon />
67- <toast.Text>
68 <Trans>Your live event preferences have been updated.</Trans>
69- </toast.Text>
70 {undoAction && (
71- <toast.Action
72 label={_(msg`Undo`)}
73 onPress={() => {
74 if (undoAction) {
···76 }
77 }}>
78 <Trans>Undo</Trans>
79- </toast.Action>
80 )}
81- </toast.Outer>,
82- {
83- type: 'success',
84- },
85 )
8687 /*
···148 </ButtonText>
149 {isHidingAllFeeds && <ButtonIcon icon={Loader} />}
150 </Button>
151- {isNative && (
152 <Button
153 label={_(msg`Cancel`)}
154 size="large"
···3import {useLingui} from '@lingui/react'
45import {useCleanError} from '#/lib/hooks/useCleanError'
06import {atoms as a, web} from '#/alf'
7import {Admonition} from '#/components/Admonition'
8import {Button, ButtonIcon, ButtonText} from '#/components/Button'
9import * as Dialog from '#/components/Dialog'
10import {Loader} from '#/components/Loader'
11+import * as Toast from '#/components/Toast'
12import {Span, Text} from '#/components/Typography'
13+import {IS_NATIVE} from '#/env'
14import {useUpdateLiveEventPreferences} from '#/features/liveEvents/preferences'
15import {
16 type LiveEventFeed,
···61 feed,
62 metricContext,
63 onUpdateSuccess({undoAction}) {
64+ Toast.show(
65+ <Toast.Outer>
66+ <Toast.Icon />
67+ <Toast.Text>
68 <Trans>Your live event preferences have been updated.</Trans>
69+ </Toast.Text>
70 {undoAction && (
71+ <Toast.Action
72 label={_(msg`Undo`)}
73 onPress={() => {
74 if (undoAction) {
···76 }
77 }}>
78 <Trans>Undo</Trans>
79+ </Toast.Action>
80 )}
81+ </Toast.Outer>,
82+ {type: 'success'},
0083 )
8485 /*
···146 </ButtonText>
147 {isHidingAllFeeds && <ButtonIcon icon={Loader} />}
148 </Button>
149+ {IS_NATIVE && (
150 <Button
151 label={_(msg`Cancel`)}
152 size="large"
+2-2
src/features/liveEvents/preferences.ts
···3import {useMutation, useQueryClient} from '@tanstack/react-query'
45import {logger} from '#/logger'
6-import {isWeb} from '#/platform/detection'
7import {
8 preferencesQueryKey,
9 usePreferencesQuery,
10} from '#/state/queries/preferences'
11import {useAgent} from '#/state/session'
012import * as env from '#/env'
13import {
14 type LiveEventFeed,
···41 const agent = useAgent()
4243 useEffect(() => {
44- if (env.IS_DEV && isWeb && typeof window !== 'undefined') {
45 // @ts-ignore
46 window.__updateLiveEventPreferences = async (
47 action: LiveEventPreferencesAction,
···3import {useMutation, useQueryClient} from '@tanstack/react-query'
45import {logger} from '#/logger'
06import {
7 preferencesQueryKey,
8 usePreferencesQuery,
9} from '#/state/queries/preferences'
10import {useAgent} from '#/state/session'
11+import {IS_WEB} from '#/env'
12import * as env from '#/env'
13import {
14 type LiveEventFeed,
···41 const agent = useAgent()
4243 useEffect(() => {
44+ if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') {
45 // @ts-ignore
46 window.__updateLiveEventPreferences = async (
47 action: LiveEventPreferencesAction,
+2-2
src/geolocation/device.ts
···3import * as Location from 'expo-location'
4import {createPermissionHook} from 'expo-modules-core'
56-import {isNative} from '#/platform/detection'
7import * as debug from '#/geolocation/debug'
8import {logger} from '#/geolocation/logger'
9import {type Geolocation} from '#/geolocation/types'
···118 const synced = useRef(false)
119 const [status] = useForegroundPermissions()
120 useEffect(() => {
121- if (!isNative) return
122123 async function get() {
124 // no need to set this more than once per session
···3import * as Location from 'expo-location'
4import {createPermissionHook} from 'expo-modules-core'
56+import {IS_NATIVE} from '#/env'
7import * as debug from '#/geolocation/debug'
8import {logger} from '#/geolocation/logger'
9import {type Geolocation} from '#/geolocation/types'
···118 const synced = useRef(false)
119 const [status] = useForegroundPermissions()
120 useEffect(() => {
121+ if (!IS_NATIVE) return
122123 async function get() {
124 // no need to set this more than once per session
···1import {type LocationGeocodedAddress} from 'expo-location'
23-import {isAndroid} from '#/platform/detection'
4import {logger} from '#/geolocation/logger'
5import {type Geolocation} from '#/geolocation/types'
6···81 /*
82 * Android doesn't give us ISO 3166-2 short codes. We need these for US
83 */
84- if (isAndroid) {
85 if (region && isoCountryCode === 'US') {
86 /*
87 * We need short codes for US states. If we can't remap it, just drop it
···1import {type LocationGeocodedAddress} from 'expo-location'
23+import {IS_ANDROID} from '#/env'
4import {logger} from '#/geolocation/logger'
5import {type Geolocation} from '#/geolocation/types'
6···81 /*
82 * Android doesn't give us ISO 3166-2 short codes. We need these for US
83 */
84+ if (IS_ANDROID) {
85 if (region && isoCountryCode === 'US') {
86 /*
87 * We need short codes for US states. If we can't remap it, just drop it
···9import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
10import {logger as notyLogger} from '#/lib/notifications/util'
11import {type NavigationProp} from '#/lib/routes/types'
12-import {isAndroid, isIOS} from '#/platform/detection'
13import {useCurrentConvoId} from '#/state/messages/current-convo-id'
14import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
15import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
···17import {useSession} from '#/state/session'
18import {useLoggedOutViewControls} from '#/state/shell/logged-out'
19import {useCloseAllActiveElements} from '#/state/util'
020import {resetToTab} from '#/Navigation'
21import {router} from '#/routes'
22···90 // channels allow for the mute/unmute functionality we want for the background
91 // handler.
92 useEffect(() => {
93- if (!isAndroid) return
94 // assign both chat notifications to a group
95 // NOTE: I don't think that it will retroactively move them into the group
96 // if the channels already exist. no big deal imo -sfn
···379 }
380381 const payload = (
382- isIOS ? e.request.trigger.payload : e.request.content.data
383 ) as NotificationPayload
384385 if (payload && payload.reason) {
···9import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
10import {logger as notyLogger} from '#/lib/notifications/util'
11import {type NavigationProp} from '#/lib/routes/types'
012import {useCurrentConvoId} from '#/state/messages/current-convo-id'
13import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
14import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
···16import {useSession} from '#/state/session'
17import {useLoggedOutViewControls} from '#/state/shell/logged-out'
18import {useCloseAllActiveElements} from '#/state/util'
19+import {IS_ANDROID, IS_IOS} from '#/env'
20import {resetToTab} from '#/Navigation'
21import {router} from '#/routes'
22···90 // channels allow for the mute/unmute functionality we want for the background
91 // handler.
92 useEffect(() => {
93+ if (!IS_ANDROID) return
94 // assign both chat notifications to a group
95 // NOTE: I don't think that it will retroactively move them into the group
96 // if the channels already exist. no big deal imo -sfn
···379 }
380381 const payload = (
382+ IS_IOS ? e.request.trigger.payload : e.request.content.data
383 ) as NotificationPayload
384385 if (payload && payload.reason) {
+4-4
src/lib/hooks/useOTAUpdates.ts
···1213import {isNetworkError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15-import {isAndroid, isIOS} from '#/platform/detection'
16import {IS_TESTFLIGHT} from '#/env'
1718const MINIMUM_MINIMIZE_TIME = 15 * 60e3
1920async function setExtraParams() {
21 await setExtraParamAsync(
22- isIOS ? 'ios-build-number' : 'android-build-number',
23 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
24 // This just ensures it gets passed as a string
25 `${nativeBuildVersion}`,
···3233async function setExtraParamsPullRequest(channel: string) {
34 await setExtraParamAsync(
35- isIOS ? 'ios-build-number' : 'android-build-number',
36 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
37 // This just ensures it gets passed as a string
38 `${nativeBuildVersion}`,
···198 // `maintainVisibleContentPosition`. See repro repo for more details:
199 // https://github.com/mozzius/ota-crash-repro
200 // Old Arch only - re-enable once we're on the New Archictecture! -sfn
201- if (isAndroid) return
202203 const subscription = AppState.addEventListener(
204 'change',
···1213import {isNetworkError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15+import {IS_ANDROID, IS_IOS} from '#/env'
16import {IS_TESTFLIGHT} from '#/env'
1718const MINIMUM_MINIMIZE_TIME = 15 * 60e3
1920async function setExtraParams() {
21 await setExtraParamAsync(
22+ IS_IOS ? 'ios-build-number' : 'android-build-number',
23 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
24 // This just ensures it gets passed as a string
25 `${nativeBuildVersion}`,
···3233async function setExtraParamsPullRequest(channel: string) {
34 await setExtraParamAsync(
35+ IS_IOS ? 'ios-build-number' : 'android-build-number',
36 // Hilariously, `buildVersion` is not actually a string on Android even though the TS type says it is.
37 // This just ensures it gets passed as a string
38 `${nativeBuildVersion}`,
···198 // `maintainVisibleContentPosition`. See repro repo for more details:
199 // https://github.com/mozzius/ota-crash-repro
200 // Old Arch only - re-enable once we're on the New Archictecture! -sfn
201+ if (IS_ANDROID) return
202203 const subscription = AppState.addEventListener(
204 'change',
+2-2
src/lib/hooks/useOpenLink.ts
···12 toNiceDomain,
13} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
15-import {isNative} from '#/platform/detection'
16import {useInAppBrowser} from '#/state/preferences/in-app-browser'
17import {useTheme} from '#/alf'
18import {useDialogContext} from '#/components/Dialog'
19import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
02021export function useOpenLink() {
22 const enabled = useInAppBrowser()
···41 }
42 }
4344- if (isNative && !url.startsWith('mailto:')) {
45 if (override === undefined && enabled === undefined) {
46 // consent dialog is a global dialog, and while it's possible to nest dialogs,
47 // the actual components need to be nested. sibling dialogs on iOS are not supported.
···12 toNiceDomain,
13} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
015import {useInAppBrowser} from '#/state/preferences/in-app-browser'
16import {useTheme} from '#/alf'
17import {useDialogContext} from '#/components/Dialog'
18import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
19+import {IS_NATIVE} from '#/env'
2021export function useOpenLink() {
22 const enabled = useInAppBrowser()
···41 }
42 }
4344+ if (IS_NATIVE && !url.startsWith('mailto:')) {
45 if (override === undefined && enabled === undefined) {
46 // consent dialog is a global dialog, and while it's possible to nest dialogs,
47 // the actual components need to be nested. sibling dialogs on iOS are not supported.
+3-3
src/lib/hooks/usePermissions.ts
···2import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
3import * as MediaLibrary from 'expo-media-library'
45-import {isWeb} from '#/platform/detection'
6import {Alert} from '#/view/com/util/Alert'
078const openPermissionAlert = (perm: string) => {
9 Alert.alert(
···26 const requestPhotoAccessIfNeeded = async () => {
27 // On the, we use <input type="file"> to produce a filepicker
28 // This does not need any permission granting.
29- if (isWeb) {
30 return true
31 }
32···55 const requestVideoAccessIfNeeded = async () => {
56 // On the, we use <input type="file"> to produce a filepicker
57 // This does not need any permission granting.
58- if (isWeb) {
59 return true
60 }
61
···2import {useCameraPermissions as useExpoCameraPermissions} from 'expo-camera'
3import * as MediaLibrary from 'expo-media-library'
405import {Alert} from '#/view/com/util/Alert'
6+import {IS_WEB} from '#/env'
78const openPermissionAlert = (perm: string) => {
9 Alert.alert(
···26 const requestPhotoAccessIfNeeded = async () => {
27 // On the, we use <input type="file"> to produce a filepicker
28 // This does not need any permission granting.
29+ if (IS_WEB) {
30 return true
31 }
32···55 const requestVideoAccessIfNeeded = async () => {
56 // On the, we use <input type="file"> to produce a filepicker
57 // This does not need any permission granting.
58+ if (IS_WEB) {
59 return true
60 }
61
+2-2
src/lib/hooks/useTranslate.ts
···2import * as IntentLauncher from 'expo-intent-launcher'
34import {getTranslatorLink} from '#/locale/helpers'
5-import {isAndroid} from '#/platform/detection'
6import {useOpenLink} from './useOpenLink'
78export function useTranslate() {
···11 return useCallback(
12 async (text: string, language: string) => {
13 const translateUrl = getTranslatorLink(text, language)
14- if (isAndroid) {
15 try {
16 // use getApplicationIconAsync to determine if the translate app is installed
17 if (
···2import * as IntentLauncher from 'expo-intent-launcher'
34import {getTranslatorLink} from '#/locale/helpers'
5+import {IS_ANDROID} from '#/env'
6import {useOpenLink} from './useOpenLink'
78export function useTranslate() {
···11 return useCallback(
12 async (text: string, language: string) => {
13 const translateUrl = getTranslatorLink(text, language)
14+ if (IS_ANDROID) {
15 try {
16 // use getApplicationIconAsync to determine if the translate app is installed
17 if (
···1819import {POST_IMG_MAX} from '#/lib/constants'
20import {logger} from '#/logger'
21-import {isAndroid, isIOS} from '#/platform/detection'
22import {type PickerImage} from './picker.shared'
23import {type Dimensions} from './types'
24import {mimeToExt} from './video/util'
···109110 // save
111 try {
112- if (isAndroid) {
113 // android triggers an annoying permission prompt if you try and move an image
114 // between albums. therefore, we need to either create the album with the image
115 // as the starting image, or put it directly into the album
···327}
328329function normalizePath(str: string, allPlatforms = false): string {
330- if (isAndroid || allPlatforms) {
331 if (!str.startsWith('file://')) {
332 return `file://${str}`
333 }
···350 type: string,
351) {
352 try {
353- if (isIOS) {
354 await withTempFile(filename, encoded, async tmpFileUrl => {
355 await Sharing.shareAsync(tmpFileUrl, {UTI: type})
356 })
···1819import {POST_IMG_MAX} from '#/lib/constants'
20import {logger} from '#/logger'
21+import {IS_ANDROID, IS_IOS} from '#/env'
22import {type PickerImage} from './picker.shared'
23import {type Dimensions} from './types'
24import {mimeToExt} from './video/util'
···109110 // save
111 try {
112+ if (IS_ANDROID) {
113 // android triggers an annoying permission prompt if you try and move an image
114 // between albums. therefore, we need to either create the album with the image
115 // as the starting image, or put it directly into the album
···327}
328329function normalizePath(str: string, allPlatforms = false): string {
330+ if (IS_ANDROID || allPlatforms) {
331 if (!str.startsWith('file://')) {
332 return `file://${str}`
333 }
···350 type: string,
351) {
352 try {
353+ if (IS_IOS) {
354 await withTempFile(filename, encoded, async tmpFileUrl => {
355 await Sharing.shareAsync(tmpFileUrl, {UTI: type})
356 })
+1-1
src/lib/media/picker.e2e.tsx
···3 getInfoAsync,
4 readDirectoryAsync,
5} from 'expo-file-system/legacy'
06import ExpoImageCropTool, {
7 type OpenCropperOptions,
8} from '@bsky.app/expo-image-crop-tool'
910import {compressIfNeeded} from './manip'
11import {type PickerImage} from './picker.shared'
12-import {ImagePickerResult} from 'expo-image-picker'
1314async function getFile() {
15 const imagesDir = documentDirectory!
···3 getInfoAsync,
4 readDirectoryAsync,
5} from 'expo-file-system/legacy'
6+import {type ImagePickerResult} from 'expo-image-picker'
7import ExpoImageCropTool, {
8 type OpenCropperOptions,
9} from '@bsky.app/expo-image-crop-tool'
1011import {compressIfNeeded} from './manip'
12import {type PickerImage} from './picker.shared'
01314async function getFile() {
15 const imagesDir = documentDirectory!
+3-3
src/lib/media/picker.shared.ts
···5} from 'expo-image-picker'
6import {t} from '@lingui/macro'
78-import {isIOS, isWeb} from '#/platform/detection'
9import {type ImageMeta} from '#/state/gallery'
10import * as Toast from '#/view/com/util/Toast'
011import {VIDEO_MAX_DURATION_MS} from '../constants'
12import {getDataUriSize} from './util'
13···53 quality: 1,
54 allowsMultipleSelection: true,
55 legacy: true,
56- base64: isWeb,
57- selectionLimit: isIOS ? selectionCountRemaining : undefined,
58 preferredAssetRepresentationMode:
59 UIImagePickerPreferredAssetRepresentationMode.Automatic,
60 videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
···5} from 'expo-image-picker'
6import {t} from '@lingui/macro'
708import {type ImageMeta} from '#/state/gallery'
9import * as Toast from '#/view/com/util/Toast'
10+import {IS_IOS, IS_WEB} from '#/env'
11import {VIDEO_MAX_DURATION_MS} from '../constants'
12import {getDataUriSize} from './util'
13···53 quality: 1,
54 allowsMultipleSelection: true,
55 legacy: true,
56+ base64: IS_WEB,
57+ selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
58 preferredAssetRepresentationMode:
59 UIImagePickerPreferredAssetRepresentationMode.Automatic,
60 videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
+2-2
src/lib/media/save-image.ios.ts
···2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
45-import {isNative} from '#/platform/detection'
6import * as Toast from '#/components/Toast'
07import {saveImageToMediaLibrary} from './manip'
89/**
···16 const {_} = useLingui()
17 return useCallback(
18 async (uri: string) => {
19- if (!isNative) {
20 throw new Error('useSaveImageToMediaLibrary is native only')
21 }
22
···2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
405import * as Toast from '#/components/Toast'
6+import {IS_NATIVE} from '#/env'
7import {saveImageToMediaLibrary} from './manip'
89/**
···16 const {_} = useLingui()
17 return useCallback(
18 async (uri: string) => {
19+ if (!IS_NATIVE) {
20 throw new Error('useSaveImageToMediaLibrary is native only')
21 }
22
+2-2
src/lib/media/save-image.ts
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
56-import {isNative} from '#/platform/detection'
7import * as Toast from '#/components/Toast'
08import {saveImageToMediaLibrary} from './manip'
910/**
···18 })
19 return useCallback(
20 async (uri: string) => {
21- if (!isNative) {
22 throw new Error('useSaveImageToMediaLibrary is native only')
23 }
24
···3import {msg} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
506import * as Toast from '#/components/Toast'
7+import {IS_NATIVE} from '#/env'
8import {saveImageToMediaLibrary} from './manip'
910/**
···18 })
19 return useCallback(
20 async (uri: string) => {
21+ if (!IS_NATIVE) {
22 throw new Error('useSaveImageToMediaLibrary is native only')
23 }
24
+5-5
src/lib/notifications/notifications.ts
···13} from '#/lib/constants'
14import {logger as notyLogger} from '#/lib/notifications/util'
15import {isNetworkError} from '#/lib/strings/errors'
16-import {isNative} from '#/platform/detection'
17import {type SessionAccount, useAgent, useSession} from '#/state/session'
18import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
19import {useAgeAssurance} from '#/ageAssurance'
020import {IS_DEV} from '#/env'
2122/**
···140 }: {
141 isAgeRestricted?: boolean
142 } = {}) => {
143- if (!isNative || IS_DEV) return
144145 /**
146 * This will also fire the listener added via `addPushTokenListener`. That
···236 const permissions = await Notifications.getPermissionsAsync()
237238 if (
239- !isNative ||
240 permissions?.status === 'granted' ||
241 (permissions?.status === 'denied' && !permissions.canAskAgain)
242 ) {
···277}
278279export async function decrementBadgeCount(by: number) {
280- if (!isNative) return
281282 let count = await getBadgeCountAsync()
283 count -= by
···295}
296297export async function unregisterPushToken(agents: AtpAgent[]) {
298- if (!isNative) return
299300 try {
301 const token = await getPushToken()
···13} from '#/lib/constants'
14import {logger as notyLogger} from '#/lib/notifications/util'
15import {isNetworkError} from '#/lib/strings/errors'
016import {type SessionAccount, useAgent, useSession} from '#/state/session'
17import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler'
18import {useAgeAssurance} from '#/ageAssurance'
19+import {IS_NATIVE} from '#/env'
20import {IS_DEV} from '#/env'
2122/**
···140 }: {
141 isAgeRestricted?: boolean
142 } = {}) => {
143+ if (!IS_NATIVE || IS_DEV) return
144145 /**
146 * This will also fire the listener added via `addPushTokenListener`. That
···236 const permissions = await Notifications.getPermissionsAsync()
237238 if (
239+ !IS_NATIVE ||
240 permissions?.status === 'granted' ||
241 (permissions?.status === 'denied' && !permissions.canAskAgain)
242 ) {
···277}
278279export async function decrementBadgeCount(by: number) {
280+ if (!IS_NATIVE) return
281282 let count = await getBadgeCountAsync()
283 count -= by
···295}
296297export async function unregisterPushToken(agents: AtpAgent[]) {
298+ if (!IS_NATIVE) return
299300 try {
301 const token = await getPushToken()
+3-3
src/lib/react-query.tsx
···9} from '@tanstack/react-query-persist-client'
10import type React from 'react'
1112-import {isNative, isWeb} from '#/platform/detection'
13import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
14import {PUBLIC_BSKY_SERVICE} from './constants'
01516declare global {
17 interface Window {
···88}, 2000)
8990focusManager.setEventListener(onFocus => {
91- if (isNative) {
92 const subscription = AppState.addEventListener(
93 'change',
94 (status: AppStateStatus) => {
···188 }
189 })
190 useEffect(() => {
191- if (isWeb) {
192 window.__TANSTACK_QUERY_CLIENT__ = queryClient
193 }
194 }, [queryClient])
···9} from '@tanstack/react-query-persist-client'
10import type React from 'react'
11012import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events'
13import {PUBLIC_BSKY_SERVICE} from './constants'
14+import {IS_NATIVE, IS_WEB} from '#/env'
1516declare global {
17 interface Window {
···88}, 2000)
8990focusManager.setEventListener(onFocus => {
91+ if (IS_NATIVE) {
92 const subscription = AppState.addEventListener(
93 'change',
94 (status: AppStateStatus) => {
···188 }
189 })
190 useEffect(() => {
191+ if (IS_WEB) {
192 window.__TANSTACK_QUERY_CLIENT__ = queryClient
193 }
194 }, [queryClient])
+4-4
src/lib/sharing.ts
···4// TODO: replace global i18n instance with one returned from useLingui -sfn
5import {t} from '@lingui/macro'
67-import {isAndroid, isIOS} from '#/platform/detection'
8import * as Toast from '#/view/com/util/Toast'
0910/**
11 * This function shares a URL using the native Share API if available, or copies it to the clipboard
···14 * clipboard.
15 */
16export async function shareUrl(url: string) {
17- if (isAndroid) {
18 await Share.share({message: url})
19- } else if (isIOS) {
20 await Share.share({url})
21 } else {
22 // React Native Share is not supported by web. Web Share API
···34 * clipboard.
35 */
36export async function shareText(text: string) {
37- if (isAndroid || isIOS) {
38 await Share.share({message: text})
39 } else {
40 await setStringAsync(text)
···4// TODO: replace global i18n instance with one returned from useLingui -sfn
5import {t} from '@lingui/macro'
607import * as Toast from '#/view/com/util/Toast'
8+import {IS_ANDROID, IS_IOS} from '#/env'
910/**
11 * This function shares a URL using the native Share API if available, or copies it to the clipboard
···14 * clipboard.
15 */
16export async function shareUrl(url: string) {
17+ if (IS_ANDROID) {
18 await Share.share({message: url})
19+ } else if (IS_IOS) {
20 await Share.share({url})
21 } else {
22 // React Native Share is not supported by web. Web Share API
···34 * clipboard.
35 */
36export async function shareText(text: string) {
37+ if (IS_ANDROID || IS_IOS) {
38 await Share.share({message: text})
39 } else {
40 await setStringAsync(text)
···56import {logger} from '#/logger'
7import {type MetricEvents} from '#/logger/metrics'
8-import {isWeb} from '#/platform/detection'
9import * as persisted from '#/state/persisted'
10-// import {useSession} from '../../state/session'
11import * as env from '#/env'
12import {device} from '#/storage'
13import {timeout} from '../async/timeout'
···3839let refSrc = ''
40let refUrl = ''
41-if (isWeb && typeof window !== 'undefined') {
42 const params = new URLSearchParams(window.location.search)
43 refSrc = params.get('ref_src') ?? ''
44 refUrl = decodeURIComponent(params.get('ref_url') ?? '')
···56import {logger} from '#/logger'
7import {type MetricEvents} from '#/logger/metrics'
08import * as persisted from '#/state/persisted'
9+import {IS_WEB} from '#/env'
10import * as env from '#/env'
11import {device} from '#/storage'
12import {timeout} from '../async/timeout'
···3738let refSrc = ''
39let refUrl = ''
40+if (IS_WEB && typeof window !== 'undefined') {
41 const params = new URLSearchParams(window.location.search)
42 refSrc = params.get('ref_src') ?? ''
43 refUrl = decodeURIComponent(params.get('ref_url') ?? '')
+5-6
src/lib/strings/embed-player.ts
···1import {Dimensions} from 'react-native'
23-import {isSafari} from '#/lib/browser'
4-import {isWeb} from '#/platform/detection'
56const {height: SCREEN_HEIGHT} = Dimensions.get('window')
78-const IFRAME_HOST = isWeb
9 ? // @ts-ignore only for web
10 window.location.host === 'localhost:8100'
11 ? 'http://localhost:8100'
···135 urlp.hostname === 'www.twitch.tv' ||
136 urlp.hostname === 'm.twitch.tv'
137 ) {
138- const parent = isWeb
139 ? // @ts-ignore only for web
140 window.location.hostname
141 : 'localhost'
···570 width: Number(w),
571 }
572573- if (isWeb) {
574- if (isSafari) {
575 id = id.replace('AAAAC', 'AAAP1')
576 filename = filename.replace('.gif', '.mp4')
577 } else {
···1import {Dimensions} from 'react-native'
23+import {IS_WEB, IS_WEB_SAFARI} from '#/env'
045const {height: SCREEN_HEIGHT} = Dimensions.get('window')
67+const IFRAME_HOST = IS_WEB
8 ? // @ts-ignore only for web
9 window.location.host === 'localhost:8100'
10 ? 'http://localhost:8100'
···134 urlp.hostname === 'www.twitch.tv' ||
135 urlp.hostname === 'm.twitch.tv'
136 ) {
137+ const parent = IS_WEB
138 ? // @ts-ignore only for web
139 window.location.hostname
140 : 'localhost'
···569 width: Number(w),
570 }
571572+ if (IS_WEB) {
573+ if (IS_WEB_SAFARI) {
574 id = id.replace('AAAAC', 'AAAP1')
575 filename = filename.replace('.gif', '.mp4')
576 } else {
+2-2
src/lib/styles.ts
···5 type TextStyle,
6} from 'react-native'
78-import {isWeb} from '#/platform/detection'
9import {type Theme, type TypographyVariant} from './ThemeContext'
1011// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
···186 // dimensions
187 w100pct: {width: '100%'},
188 h100pct: {height: '100%'},
189- hContentRegion: isWeb ? {minHeight: '100%'} : {height: '100%'},
190 window: {
191 width: Dimensions.get('window').width,
192 height: Dimensions.get('window').height,
···5 type TextStyle,
6} from 'react-native'
78+import {IS_WEB} from '#/env'
9import {type Theme, type TypographyVariant} from './ThemeContext'
1011// 1 is lightest, 2 is light, 3 is mid, 4 is dark, 5 is darkest
···186 // dimensions
187 w100pct: {width: '100%'},
188 h100pct: {height: '100%'},
189+ hContentRegion: IS_WEB ? {minHeight: '100%'} : {height: '100%'},
190 window: {
191 width: Dimensions.get('window').width,
192 height: Dimensions.get('window').height,
+139-139
src/locale/locales/en/messages.po
···18msgid "\"{interestsDisplayName}\" category (active)"
19msgstr ""
2021-#: src/screens/Messages/components/ChatListItem.tsx:163
22msgid "(contains embedded content)"
23msgstr ""
24···163msgid "{0} is not available"
164msgstr ""
165166-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:232
167msgid "{0} joined this week"
168msgstr ""
169···180msgid "{0} reacted {1}"
181msgstr ""
182183-#: src/screens/Messages/components/ChatListItem.tsx:234
184msgid "{0} reacted {1} to {2}"
185msgstr ""
186···192msgid "{0}, a list by {1}"
193msgstr ""
194195-#: src/view/com/util/UserAvatar.tsx:578
196-#: src/view/com/util/UserAvatar.tsx:596
197msgid "{0}'s avatar"
198msgstr ""
199···540msgstr ""
541542#. If last message does not contain text, fall back to "{user} reacted to {a message}"
543-#: src/screens/Messages/components/ChatListItem.tsx:213
544msgid "a message"
545msgstr ""
546···714msgid "Add a content warning"
715msgstr ""
716717-#: src/components/live/GoLiveDialog.tsx:103
718msgid "Add a temporary live status to your profile. When someone clicks on your avatar, they’ll see information about your live event."
719msgstr ""
720···738msgid "Add alt text"
739msgstr ""
740741-#: src/view/com/composer/videos/SubtitleDialog.tsx:110
742msgid "Add alt text (optional)"
743msgstr ""
744···826msgid "Add this feed to your feeds"
827msgstr ""
828829-#: src/view/com/profile/ProfileMenu.tsx:340
830-#: src/view/com/profile/ProfileMenu.tsx:343
831msgid "Add to lists"
832msgstr ""
833···836msgstr ""
837838#: src/components/dialogs/StarterPackDialog.tsx:176
839-#: src/view/com/profile/ProfileMenu.tsx:331
840-#: src/view/com/profile/ProfileMenu.tsx:334
841msgid "Add to starter packs"
842msgstr ""
843···1022#: src/view/com/composer/GifAltText.tsx:154
1023#: src/view/com/composer/photos/ImageAltTextDialog.tsx:117
1024#: src/view/com/composer/videos/SubtitleDialog.tsx:40
1025-#: src/view/com/composer/videos/SubtitleDialog.tsx:55
1026-#: src/view/com/composer/videos/SubtitleDialog.tsx:105
1027#: src/view/com/composer/videos/SubtitleDialog.tsx:109
01028msgid "Alt text"
1029msgstr ""
1030···1036msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
1037msgstr ""
10381039-#: src/view/com/composer/videos/SubtitleDialog.tsx:133
1040msgid "Alt text must be less than {MAX_ALT_TEXT} characters."
1041msgstr ""
1042···1054msgid "An error has occurred"
1055msgstr ""
10561057-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:422
1058msgid "An error occurred"
1059msgstr ""
1060···1151msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name."
1152msgstr ""
11531154-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:166
1155msgid "An unknown error occurred."
1156msgstr ""
1157···14761477#: src/components/PostControls/PostMenu/PostMenuItems.tsx:818
1478#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190
1479-#: src/view/com/profile/ProfileMenu.tsx:548
1480msgid "Block"
1481msgstr ""
1482···1486#: src/components/PostControls/PostMenu/PostMenuItems.tsx:705
1487#: src/screens/Messages/components/RequestButtons.tsx:144
1488#: src/screens/Messages/components/RequestButtons.tsx:146
1489-#: src/view/com/profile/ProfileMenu.tsx:454
1490-#: src/view/com/profile/ProfileMenu.tsx:461
1491msgid "Block account"
1492msgstr ""
14931494#: src/components/PostControls/PostMenu/PostMenuItems.tsx:813
1495-#: src/view/com/profile/ProfileMenu.tsx:531
1496msgid "Block Account?"
1497msgstr ""
1498···1544msgstr ""
15451546#: src/components/PostControls/PostMenu/PostMenuItems.tsx:815
1547-#: src/view/com/profile/ProfileMenu.tsx:543
1548msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."
1549msgstr ""
1550···1560msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."
1561msgstr ""
15621563-#: src/view/com/profile/ProfileMenu.tsx:540
1564msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you."
1565msgstr ""
1566···1761#: src/components/dialogs/InAppBrowserConsent.tsx:104
1762#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:274
1763#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:282
1764-#: src/components/live/GoLiveDialog.tsx:243
1765-#: src/components/live/GoLiveDialog.tsx:249
1766#: src/components/Menu/index.tsx:352
1767#: src/components/PostControls/RepostButton.tsx:209
1768#: src/components/Prompt.tsx:144
1769#: src/components/Prompt.tsx:146
1770-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:153
1771-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:158
1772#: src/lib/media/picker.tsx:38
1773#: src/screens/Deactivated.tsx:149
1774#: src/screens/Profile/Header/EditProfileDialog.tsx:218
···1821msgid "Cannot interact with a blocked user"
1822msgstr ""
18231824-#: src/view/com/composer/videos/SubtitleDialog.tsx:148
1825msgid "Captions (.vtt)"
1826msgstr ""
18271828#: src/view/com/composer/videos/SubtitleDialog.tsx:40
1829-#: src/view/com/composer/videos/SubtitleDialog.tsx:55
1830msgid "Captions & alt text"
1831msgstr ""
1832···2431msgid "Conversation deleted"
2432msgstr ""
24332434-#: src/screens/Messages/components/ChatListItem.tsx:199
2435msgid "Conversation deleted"
2436msgstr ""
2437···2465msgid "Copy App Password"
2466msgstr ""
24672468-#: src/view/com/profile/ProfileMenu.tsx:491
2469-#: src/view/com/profile/ProfileMenu.tsx:494
2470msgid "Copy at:// URI"
2471msgstr ""
2472···2481msgstr ""
24822483#: src/screens/Settings/components/ChangeHandleDialog.tsx:502
2484-#: src/view/com/profile/ProfileMenu.tsx:500
2485-#: src/view/com/profile/ProfileMenu.tsx:503
2486msgid "Copy DID"
2487msgstr ""
2488···2495msgid "Copy link"
2496msgstr ""
24972498-#: src/components/StarterPack/ShareDialog.tsx:119
2499msgid "Copy Link"
2500msgstr ""
2501···2671msgid "Create an account"
2672msgstr ""
26732674-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:312
2675-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:319
2676msgid "Create an account without using this starter pack"
2677msgstr ""
2678···2976msgid "Disable replies entirely"
2977msgstr ""
29782979-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:388
2980msgid "Disable subtitles"
2981msgstr ""
2982···3119#: src/view/com/composer/labels/LabelsBtn.tsx:218
3120#: src/view/com/composer/labels/LabelsBtn.tsx:225
3121#: src/view/com/composer/select-language/PostLanguageSelectDialog.tsx:303
3122-#: src/view/com/composer/videos/SubtitleDialog.tsx:184
3123-#: src/view/com/composer/videos/SubtitleDialog.tsx:195
3124msgid "Done"
3125msgstr ""
3126···3146msgid "Double tap to like"
3147msgstr ""
31483149-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:330
3150msgid "Download Bluesky"
3151msgstr ""
3152···3221msgid "Edit"
3222msgstr ""
32233224-#: src/view/com/util/UserAvatar.tsx:440
3225#: src/view/com/util/UserBanner.tsx:121
3226msgid "Edit avatar"
3227msgstr ""
···3251msgid "Edit list details"
3252msgstr ""
32533254-#: src/view/com/profile/ProfileMenu.tsx:354
3255-#: src/view/com/profile/ProfileMenu.tsx:374
3256msgid "Edit live status"
3257msgstr ""
3258···3408msgid "Enable quote posts of this post"
3409msgstr ""
34103411-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:389
3412msgid "Enable subtitles"
3413msgstr ""
3414···3436msgid "End of feed"
3437msgstr ""
34383439-#: src/view/com/composer/videos/SubtitleDialog.tsx:174
3440msgid "Ensure you have selected a language for each subtitle file."
3441msgstr ""
3442···3459msgid "Enter code"
3460msgstr ""
34613462-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:407
3463msgid "Enter fullscreen"
3464msgstr ""
3465···3529msgid "Error occurred while saving file"
3530msgstr ""
35313532-#: src/screens/Signup/StepCaptcha/index.tsx:123
3533msgid "Error receiving captcha response."
3534msgstr ""
3535···3565msgid "Excludes users you follow"
3566msgstr ""
35673568-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:406
3569msgid "Exit fullscreen"
3570msgstr ""
3571···4121msgid "Follow 7 accounts"
4122msgstr ""
41234124-#: src/view/com/profile/ProfileMenu.tsx:310
4125-#: src/view/com/profile/ProfileMenu.tsx:321
4126msgid "Follow account"
4127msgstr ""
4128···4433msgid "Go Home"
4434msgstr ""
44354436-#: src/view/com/profile/ProfileMenu.tsx:355
4437-#: src/view/com/profile/ProfileMenu.tsx:376
4438msgid "Go live"
4439msgstr ""
44404441-#: src/components/live/GoLiveDialog.tsx:95
4442-#: src/components/live/GoLiveDialog.tsx:100
4443-#: src/components/live/GoLiveDialog.tsx:228
4444-#: src/components/live/GoLiveDialog.tsx:237
4445msgid "Go Live"
4446msgstr ""
44474448-#: src/view/com/profile/ProfileMenu.tsx:352
4449-#: src/view/com/profile/ProfileMenu.tsx:372
4450msgid "Go live (disabled)"
4451msgstr ""
44524453-#: src/components/live/GoLiveDialog.tsx:170
4454msgid "Go live for"
4455msgstr ""
4456···4465msgid "Go to account settings"
4466msgstr ""
44674468-#: src/screens/Messages/components/ChatListItem.tsx:364
4469msgid "Go to conversation with {0}"
4470msgstr ""
4471···4644msgid "Hide"
4645msgstr ""
46464647-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:140
4648-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:147
4649msgid "Hide all events"
4650msgstr ""
4651···4676msgid "Hide this card"
4677msgstr ""
46784679-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:128
4680-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:135
4681msgid "Hide this event"
4682msgstr ""
4683···4830msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here</0>."
4831msgstr ""
48324833-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:119
4834msgid "If you choose to hide all events, you can always re-enable them from <0>Settings → Content & Media</0>."
4835msgstr ""
4836···5093msgid "Jobs"
5094msgstr ""
50955096-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:210
5097-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:216
5098#: src/screens/StarterPack/StarterPackScreen.tsx:464
5099#: src/screens/StarterPack/StarterPackScreen.tsx:475
5100msgid "Join Bluesky"
···5491msgid "Live event happening now: {0}"
5492msgstr ""
54935494-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:107
5495msgid "Live event options"
5496msgstr ""
54975498-#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:111
5499msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface."
5500msgstr ""
5501···55055506#: src/components/live/EditLiveDialog.tsx:147
5507#: src/components/live/EditLiveDialog.tsx:151
5508-#: src/components/live/GoLiveDialog.tsx:126
5509-#: src/components/live/GoLiveDialog.tsx:130
5510msgid "Live link"
5511msgstr ""
5512···5665msgid "Message deleted"
5666msgstr ""
56675668-#: src/screens/Messages/components/ChatListItem.tsx:200
5669msgid "Message deleted"
5670msgstr ""
5671···5805msgstr ""
58065807#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
5808-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:95
5809msgctxt "video"
5810msgid "Mute"
5811msgstr ""
···58175818#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686
5819#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692
5820-#: src/view/com/profile/ProfileMenu.tsx:433
5821-#: src/view/com/profile/ProfileMenu.tsx:440
5822msgid "Mute account"
5823msgstr ""
5824···5948msgstr ""
59495950#: src/screens/Search/modules/ExploreTrendingTopics.tsx:196
5951-#: src/view/com/profile/ProfileMenu.tsx:388
5952msgid "New"
5953msgstr ""
5954···6149msgid "No media yet"
6150msgstr ""
61516152-#: src/screens/Messages/components/ChatListItem.tsx:142
6153msgid "No messages yet"
6154msgstr ""
6155···6293msgid "Not Found"
6294msgstr ""
62956296-#: src/view/com/profile/ProfileMenu.tsx:555
6297msgid "Note about sharing"
6298msgstr ""
6299···6475msgid "Open camera"
6476msgstr ""
64776478-#: src/screens/Messages/components/ChatListItem.tsx:374
6479-#: src/screens/Messages/components/ChatListItem.tsx:378
6480msgid "Open conversation options"
6481msgstr ""
6482···6623msgid "Opens link {0}"
6624msgstr ""
66256626-#: src/view/com/util/UserAvatar.tsx:582
6627msgid "Opens live status dialog"
6628msgstr ""
6629···6640msgstr ""
66416642#: src/view/com/notifications/NotificationFeedItem.tsx:1027
6643-#: src/view/com/util/UserAvatar.tsx:600
6644msgid "Opens this profile"
6645msgstr ""
6646···67606761#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
6762#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
6763-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369
6764msgid "Pause"
6765msgstr ""
67666767-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:320
6768msgid "Pause video"
6769msgstr ""
6770···68716872#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
6873#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
6874-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:370
6875msgid "Play"
6876msgstr ""
6877···6880msgstr ""
68816882#: src/components/Post/Embed/VideoEmbed/index.tsx:115
6883-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:321
6884msgid "Play video"
6885msgstr ""
6886···7065msgid "Porn"
7066msgstr ""
70677068-#: src/screens/PostThread/index.tsx:532
7069msgctxt "description"
7070msgid "Post"
7071msgstr ""
···7536msgid "Remove attachment"
7537msgstr ""
75387539-#: src/view/com/util/UserAvatar.tsx:499
7540-#: src/view/com/util/UserAvatar.tsx:502
7541msgid "Remove Avatar"
7542msgstr ""
7543···7608msgid "Remove repost"
7609msgstr ""
76107611-#: src/view/com/composer/videos/SubtitleDialog.tsx:278
7612msgid "Remove subtitle file"
7613msgstr ""
7614···76277628#: src/components/verification/VerificationRemovePrompt.tsx:46
7629#: src/components/verification/VerificationsDialog.tsx:252
7630-#: src/view/com/profile/ProfileMenu.tsx:406
7631-#: src/view/com/profile/ProfileMenu.tsx:409
7632msgid "Remove verification"
7633msgstr ""
7634···7763msgid "Report"
7764msgstr ""
77657766-#: src/view/com/profile/ProfileMenu.tsx:473
7767-#: src/view/com/profile/ProfileMenu.tsx:476
7768msgid "Report account"
7769msgstr ""
7770···8074msgid "Save changes"
8075msgstr ""
80768077-#: src/components/StarterPack/ShareDialog.tsx:138
8078-#: src/components/StarterPack/ShareDialog.tsx:144
8079msgid "Save image"
8080msgstr ""
8081···8223msgid "Search my posts"
8224msgstr ""
82258226-#: src/view/com/profile/ProfileMenu.tsx:289
8227-#: src/view/com/profile/ProfileMenu.tsx:292
8228msgid "Search posts"
8229msgstr ""
8230···8350msgid "Select content languages"
8351msgstr ""
83528353-#: src/components/live/GoLiveDialog.tsx:175
8354msgid "Select duration"
8355msgstr ""
8356···8384msgid "Select language"
8385msgstr ""
83868387-#: src/view/com/composer/videos/SubtitleDialog.tsx:265
8388msgid "Select language..."
8389msgstr ""
8390···8642msgid "Share a fun fact!"
8643msgstr ""
86448645-#: src/view/com/profile/ProfileMenu.tsx:560
8646msgid "Share anyway"
8647msgstr ""
8648···8654#: src/components/dialogs/LinkWarning.tsx:96
8655#: src/components/dialogs/LinkWarning.tsx:104
8656#: src/components/StarterPack/ShareDialog.tsx:113
8657-#: src/components/StarterPack/ShareDialog.tsx:119
8658msgid "Share link"
8659msgstr ""
8660···8667msgid "Share post at:// URI"
8668msgstr ""
86698670-#: src/components/StarterPack/ShareDialog.tsx:123
8671-#: src/components/StarterPack/ShareDialog.tsx:133
8672msgid "Share QR code"
8673msgstr ""
8674···8974msgid "Someone reacted {0}"
8975msgstr ""
89768977-#: src/screens/Messages/components/ChatListItem.tsx:244
8978msgid "Someone reacted {0} to {1}"
8979msgstr ""
8980···94049405#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186
9406#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391
9407-#: src/view/com/profile/ProfileMenu.tsx:536
9408msgid "The account will be able to interact with you after unblocking."
9409msgstr ""
9410···9442msgid "The Discover feed now knows what you like"
9443msgstr ""
94449445-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:333
9446msgid "The experience is better in the app. Download Bluesky now and we'll pick back up where you left off."
9447msgstr ""
9448···9458msgid "The following labels were applied to your content."
9459msgstr ""
94609461-#: src/components/live/GoLiveDialog.tsx:157
9462msgid "The following services are enabled for your account: {allowedServices}"
9463msgstr ""
9464···9551msgstr ""
95529553#: src/screens/Search/Explore.tsx:999
9554-#: src/view/com/posts/PostFeed.tsx:759
9555msgid "There was an issue fetching posts. Tap here to try again."
9556msgstr ""
9557···9702msgid "This content is not viewable without a Bluesky account."
9703msgstr ""
97049705-#: src/screens/Messages/components/ChatListItem.tsx:366
9706msgid "This conversation is with a deleted or a deactivated account. Press for options"
9707msgstr ""
9708···9757msgstr ""
97589759#: src/components/live/EditLiveDialog.tsx:176
9760-#: src/components/live/GoLiveDialog.tsx:150
9761msgid "This is not a valid link"
9762msgstr ""
9763···9817msgid "This post's author has disabled quote posts."
9818msgstr ""
98199820-#: src/view/com/profile/ProfileMenu.tsx:557
9821msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in."
9822msgstr ""
9823···10090#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394
10091#: src/screens/ProfileList/components/Header.tsx:171
10092#: src/screens/ProfileList/components/Header.tsx:178
10093-#: src/view/com/profile/ProfileMenu.tsx:548
10094msgid "Unblock"
10095msgstr ""
10096···1010110102#: src/components/dms/ConvoMenu.tsx:261
10103#: src/components/dms/ConvoMenu.tsx:264
10104-#: src/view/com/profile/ProfileMenu.tsx:453
10105-#: src/view/com/profile/ProfileMenu.tsx:459
10106msgid "Unblock account"
10107msgstr ""
1010810109#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184
10110#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389
10111-#: src/view/com/profile/ProfileMenu.tsx:530
10112msgid "Unblock Account?"
10113msgstr ""
10114···10141msgid "Unfollow {0}"
10142msgstr ""
1014310144-#: src/view/com/profile/ProfileMenu.tsx:309
10145-#: src/view/com/profile/ProfileMenu.tsx:319
10146msgid "Unfollow account"
10147msgstr ""
10148···10184msgstr ""
1018510186#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152
10187-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:94
10188msgctxt "video"
10189msgid "Unmute"
10190msgstr ""
···1020110202#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685
10203#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691
10204-#: src/view/com/profile/ProfileMenu.tsx:432
10205-#: src/view/com/profile/ProfileMenu.tsx:438
10206msgid "Unmute account"
10207msgstr ""
10208···10220msgid "Unmute thread"
10221msgstr ""
1022210223-#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:318
10224msgid "Unmute video"
10225msgstr ""
10226···10339msgid "Upload a text file to:"
10340msgstr ""
1034110342-#: src/view/com/util/UserAvatar.tsx:470
10343-#: src/view/com/util/UserAvatar.tsx:473
10344#: src/view/com/util/UserBanner.tsx:159
10345#: src/view/com/util/UserBanner.tsx:162
10346msgid "Upload from Camera"
10347msgstr ""
1034810349-#: src/view/com/util/UserAvatar.tsx:487
10350#: src/view/com/util/UserBanner.tsx:176
10351msgid "Upload from Files"
10352msgstr ""
1035310354-#: src/view/com/util/UserAvatar.tsx:481
10355-#: src/view/com/util/UserAvatar.tsx:485
10356#: src/view/com/util/UserBanner.tsx:170
10357#: src/view/com/util/UserBanner.tsx:174
10358msgid "Upload from Library"
···1051010511#: src/components/verification/VerificationCreatePrompt.tsx:84
10512#: src/components/verification/VerificationCreatePrompt.tsx:86
10513-#: src/view/com/profile/ProfileMenu.tsx:416
10514-#: src/view/com/profile/ProfileMenu.tsx:419
10515msgid "Verify account"
10516msgstr ""
10517···10626msgid "Video not found."
10627msgstr ""
1062810629-#: src/view/com/composer/videos/SubtitleDialog.tsx:102
10630msgid "Video settings"
10631msgstr ""
10632···11120msgstr ""
1112111122#: src/components/live/EditLiveDialog.tsx:152
11123-#: src/components/live/GoLiveDialog.tsx:131
11124msgid "www.mylivestream.tv"
11125msgstr ""
11126···11464msgid "You reacted {0}"
11465msgstr ""
1146611467-#: src/screens/Messages/components/ChatListItem.tsx:221
11468msgid "You reacted {0} to {1}"
11469msgstr ""
11470···11494msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password."
11495msgstr ""
1149611497-#: src/screens/Messages/components/ChatListItem.tsx:157
11498msgid "You: {0}"
11499msgstr ""
1150011501-#: src/screens/Messages/components/ChatListItem.tsx:186
11502msgid "You: {defaultEmbeddedContentMessage}"
11503msgstr ""
1150411505-#: src/screens/Messages/components/ChatListItem.tsx:179
11506msgid "You: {short}"
11507msgstr ""
11508···11514msgid "You'll follow the suggested users once you finish creating your account!"
11515msgstr ""
1151611517-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:245
11518msgid "You'll follow these people and {0} others"
11519msgstr ""
1152011521-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:243
11522msgid "You'll follow these people right away"
11523msgstr ""
11524···11530msgid "You'll start receiving notifications for {0}!"
11531msgstr ""
1153211533-#: src/screens/StarterPack/StarterPackLandingScreen.tsx:283
11534msgid "You'll stay updated with these feeds"
11535msgstr ""
11536
···18msgid "\"{interestsDisplayName}\" category (active)"
19msgstr ""
2021+#: src/screens/Messages/components/ChatListItem.tsx:162
22msgid "(contains embedded content)"
23msgstr ""
24···163msgid "{0} is not available"
164msgstr ""
165166+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:231
167msgid "{0} joined this week"
168msgstr ""
169···180msgid "{0} reacted {1}"
181msgstr ""
182183+#: src/screens/Messages/components/ChatListItem.tsx:233
184msgid "{0} reacted {1} to {2}"
185msgstr ""
186···192msgid "{0}, a list by {1}"
193msgstr ""
194195+#: src/view/com/util/UserAvatar.tsx:577
196+#: src/view/com/util/UserAvatar.tsx:595
197msgid "{0}'s avatar"
198msgstr ""
199···540msgstr ""
541542#. If last message does not contain text, fall back to "{user} reacted to {a message}"
543+#: src/screens/Messages/components/ChatListItem.tsx:212
544msgid "a message"
545msgstr ""
546···714msgid "Add a content warning"
715msgstr ""
716717+#: src/components/live/GoLiveDialog.tsx:106
718msgid "Add a temporary live status to your profile. When someone clicks on your avatar, they’ll see information about your live event."
719msgstr ""
720···738msgid "Add alt text"
739msgstr ""
740741+#: src/view/com/composer/videos/SubtitleDialog.tsx:114
742msgid "Add alt text (optional)"
743msgstr ""
744···826msgid "Add this feed to your feeds"
827msgstr ""
828829+#: src/view/com/profile/ProfileMenu.tsx:342
830+#: src/view/com/profile/ProfileMenu.tsx:345
831msgid "Add to lists"
832msgstr ""
833···836msgstr ""
837838#: src/components/dialogs/StarterPackDialog.tsx:176
839+#: src/view/com/profile/ProfileMenu.tsx:333
840+#: src/view/com/profile/ProfileMenu.tsx:336
841msgid "Add to starter packs"
842msgstr ""
843···1022#: src/view/com/composer/GifAltText.tsx:154
1023#: src/view/com/composer/photos/ImageAltTextDialog.tsx:117
1024#: src/view/com/composer/videos/SubtitleDialog.tsx:40
1025+#: src/view/com/composer/videos/SubtitleDialog.tsx:58
01026#: src/view/com/composer/videos/SubtitleDialog.tsx:109
1027+#: src/view/com/composer/videos/SubtitleDialog.tsx:113
1028msgid "Alt text"
1029msgstr ""
1030···1036msgid "Alt text describes images for blind and low-vision users, and helps give context to everyone."
1037msgstr ""
10381039+#: src/view/com/composer/videos/SubtitleDialog.tsx:137
1040msgid "Alt text must be less than {MAX_ALT_TEXT} characters."
1041msgstr ""
1042···1054msgid "An error has occurred"
1055msgstr ""
10561057+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:421
1058msgid "An error occurred"
1059msgstr ""
1060···1151msgid "An mockup of a iPhone showing the Bluesky app open to the profile of a verified user with a blue checkmark next to their display name."
1152msgstr ""
11531154+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:164
1155msgid "An unknown error occurred."
1156msgstr ""
1157···14761477#: src/components/PostControls/PostMenu/PostMenuItems.tsx:818
1478#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:190
1479+#: src/view/com/profile/ProfileMenu.tsx:550
1480msgid "Block"
1481msgstr ""
1482···1486#: src/components/PostControls/PostMenu/PostMenuItems.tsx:705
1487#: src/screens/Messages/components/RequestButtons.tsx:144
1488#: src/screens/Messages/components/RequestButtons.tsx:146
1489+#: src/view/com/profile/ProfileMenu.tsx:456
1490+#: src/view/com/profile/ProfileMenu.tsx:463
1491msgid "Block account"
1492msgstr ""
14931494#: src/components/PostControls/PostMenu/PostMenuItems.tsx:813
1495+#: src/view/com/profile/ProfileMenu.tsx:533
1496msgid "Block Account?"
1497msgstr ""
1498···1544msgstr ""
15451546#: src/components/PostControls/PostMenu/PostMenuItems.tsx:815
1547+#: src/view/com/profile/ProfileMenu.tsx:545
1548msgid "Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."
1549msgstr ""
1550···1560msgid "Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you."
1561msgstr ""
15621563+#: src/view/com/profile/ProfileMenu.tsx:542
1564msgid "Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you."
1565msgstr ""
1566···1761#: src/components/dialogs/InAppBrowserConsent.tsx:104
1762#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:274
1763#: src/components/dialogs/lists/CreateOrEditListDialog.tsx:282
1764+#: src/components/live/GoLiveDialog.tsx:246
1765+#: src/components/live/GoLiveDialog.tsx:252
1766#: src/components/Menu/index.tsx:352
1767#: src/components/PostControls/RepostButton.tsx:209
1768#: src/components/Prompt.tsx:144
1769#: src/components/Prompt.tsx:146
1770+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:151
1771+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:156
1772#: src/lib/media/picker.tsx:38
1773#: src/screens/Deactivated.tsx:149
1774#: src/screens/Profile/Header/EditProfileDialog.tsx:218
···1821msgid "Cannot interact with a blocked user"
1822msgstr ""
18231824+#: src/view/com/composer/videos/SubtitleDialog.tsx:152
1825msgid "Captions (.vtt)"
1826msgstr ""
18271828#: src/view/com/composer/videos/SubtitleDialog.tsx:40
1829+#: src/view/com/composer/videos/SubtitleDialog.tsx:56
1830msgid "Captions & alt text"
1831msgstr ""
1832···2431msgid "Conversation deleted"
2432msgstr ""
24332434+#: src/screens/Messages/components/ChatListItem.tsx:198
2435msgid "Conversation deleted"
2436msgstr ""
2437···2465msgid "Copy App Password"
2466msgstr ""
24672468+#: src/view/com/profile/ProfileMenu.tsx:493
2469+#: src/view/com/profile/ProfileMenu.tsx:496
2470msgid "Copy at:// URI"
2471msgstr ""
2472···2481msgstr ""
24822483#: src/screens/Settings/components/ChangeHandleDialog.tsx:502
2484+#: src/view/com/profile/ProfileMenu.tsx:502
2485+#: src/view/com/profile/ProfileMenu.tsx:505
2486msgid "Copy DID"
2487msgstr ""
2488···2495msgid "Copy link"
2496msgstr ""
24972498+#: src/components/StarterPack/ShareDialog.tsx:120
2499msgid "Copy Link"
2500msgstr ""
2501···2671msgid "Create an account"
2672msgstr ""
26732674+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:311
2675+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:318
2676msgid "Create an account without using this starter pack"
2677msgstr ""
2678···2976msgid "Disable replies entirely"
2977msgstr ""
29782979+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:387
2980msgid "Disable subtitles"
2981msgstr ""
2982···3119#: src/view/com/composer/labels/LabelsBtn.tsx:218
3120#: src/view/com/composer/labels/LabelsBtn.tsx:225
3121#: src/view/com/composer/select-language/PostLanguageSelectDialog.tsx:303
3122+#: src/view/com/composer/videos/SubtitleDialog.tsx:188
3123+#: src/view/com/composer/videos/SubtitleDialog.tsx:199
3124msgid "Done"
3125msgstr ""
3126···3146msgid "Double tap to like"
3147msgstr ""
31483149+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:329
3150msgid "Download Bluesky"
3151msgstr ""
3152···3221msgid "Edit"
3222msgstr ""
32233224+#: src/view/com/util/UserAvatar.tsx:439
3225#: src/view/com/util/UserBanner.tsx:121
3226msgid "Edit avatar"
3227msgstr ""
···3251msgid "Edit list details"
3252msgstr ""
32533254+#: src/view/com/profile/ProfileMenu.tsx:356
3255+#: src/view/com/profile/ProfileMenu.tsx:376
3256msgid "Edit live status"
3257msgstr ""
3258···3408msgid "Enable quote posts of this post"
3409msgstr ""
34103411+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:388
3412msgid "Enable subtitles"
3413msgstr ""
3414···3436msgid "End of feed"
3437msgstr ""
34383439+#: src/view/com/composer/videos/SubtitleDialog.tsx:178
3440msgid "Ensure you have selected a language for each subtitle file."
3441msgstr ""
3442···3459msgid "Enter code"
3460msgstr ""
34613462+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:406
3463msgid "Enter fullscreen"
3464msgstr ""
3465···3529msgid "Error occurred while saving file"
3530msgstr ""
35313532+#: src/screens/Signup/StepCaptcha/index.tsx:125
3533msgid "Error receiving captcha response."
3534msgstr ""
3535···3565msgid "Excludes users you follow"
3566msgstr ""
35673568+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:405
3569msgid "Exit fullscreen"
3570msgstr ""
3571···4121msgid "Follow 7 accounts"
4122msgstr ""
41234124+#: src/view/com/profile/ProfileMenu.tsx:312
4125+#: src/view/com/profile/ProfileMenu.tsx:323
4126msgid "Follow account"
4127msgstr ""
4128···4433msgid "Go Home"
4434msgstr ""
44354436+#: src/view/com/profile/ProfileMenu.tsx:357
4437+#: src/view/com/profile/ProfileMenu.tsx:378
4438msgid "Go live"
4439msgstr ""
44404441+#: src/components/live/GoLiveDialog.tsx:98
4442+#: src/components/live/GoLiveDialog.tsx:103
4443+#: src/components/live/GoLiveDialog.tsx:231
4444+#: src/components/live/GoLiveDialog.tsx:240
4445msgid "Go Live"
4446msgstr ""
44474448+#: src/view/com/profile/ProfileMenu.tsx:354
4449+#: src/view/com/profile/ProfileMenu.tsx:374
4450msgid "Go live (disabled)"
4451msgstr ""
44524453+#: src/components/live/GoLiveDialog.tsx:173
4454msgid "Go live for"
4455msgstr ""
4456···4465msgid "Go to account settings"
4466msgstr ""
44674468+#: src/screens/Messages/components/ChatListItem.tsx:363
4469msgid "Go to conversation with {0}"
4470msgstr ""
4471···4644msgid "Hide"
4645msgstr ""
46464647+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:138
4648+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:145
4649msgid "Hide all events"
4650msgstr ""
4651···4676msgid "Hide this card"
4677msgstr ""
46784679+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:126
4680+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:133
4681msgid "Hide this event"
4682msgstr ""
4683···4830msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here</0>."
4831msgstr ""
48324833+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:117
4834msgid "If you choose to hide all events, you can always re-enable them from <0>Settings → Content & Media</0>."
4835msgstr ""
4836···5093msgid "Jobs"
5094msgstr ""
50955096+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:209
5097+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:215
5098#: src/screens/StarterPack/StarterPackScreen.tsx:464
5099#: src/screens/StarterPack/StarterPackScreen.tsx:475
5100msgid "Join Bluesky"
···5491msgid "Live event happening now: {0}"
5492msgstr ""
54935494+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:105
5495msgid "Live event options"
5496msgstr ""
54975498+#: src/features/liveEvents/components/LiveEventFeedOptionsMenu.tsx:109
5499msgid "Live events appear occasionally when something exciting is happening. If you'd like, you can hide this particular event, or all events for this placement in your app interface."
5500msgstr ""
5501···55055506#: src/components/live/EditLiveDialog.tsx:147
5507#: src/components/live/EditLiveDialog.tsx:151
5508+#: src/components/live/GoLiveDialog.tsx:129
5509+#: src/components/live/GoLiveDialog.tsx:133
5510msgid "Live link"
5511msgstr ""
5512···5665msgid "Message deleted"
5666msgstr ""
56675668+#: src/screens/Messages/components/ChatListItem.tsx:199
5669msgid "Message deleted"
5670msgstr ""
5671···5805msgstr ""
58065807#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:153
5808+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:97
5809msgctxt "video"
5810msgid "Mute"
5811msgstr ""
···58175818#: src/components/PostControls/PostMenu/PostMenuItems.tsx:686
5819#: src/components/PostControls/PostMenu/PostMenuItems.tsx:692
5820+#: src/view/com/profile/ProfileMenu.tsx:435
5821+#: src/view/com/profile/ProfileMenu.tsx:442
5822msgid "Mute account"
5823msgstr ""
5824···5948msgstr ""
59495950#: src/screens/Search/modules/ExploreTrendingTopics.tsx:196
5951+#: src/view/com/profile/ProfileMenu.tsx:390
5952msgid "New"
5953msgstr ""
5954···6149msgid "No media yet"
6150msgstr ""
61516152+#: src/screens/Messages/components/ChatListItem.tsx:141
6153msgid "No messages yet"
6154msgstr ""
6155···6293msgid "Not Found"
6294msgstr ""
62956296+#: src/view/com/profile/ProfileMenu.tsx:557
6297msgid "Note about sharing"
6298msgstr ""
6299···6475msgid "Open camera"
6476msgstr ""
64776478+#: src/screens/Messages/components/ChatListItem.tsx:373
6479+#: src/screens/Messages/components/ChatListItem.tsx:377
6480msgid "Open conversation options"
6481msgstr ""
6482···6623msgid "Opens link {0}"
6624msgstr ""
66256626+#: src/view/com/util/UserAvatar.tsx:581
6627msgid "Opens live status dialog"
6628msgstr ""
6629···6640msgstr ""
66416642#: src/view/com/notifications/NotificationFeedItem.tsx:1027
6643+#: src/view/com/util/UserAvatar.tsx:599
6644msgid "Opens this profile"
6645msgstr ""
6646···67606761#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
6762#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
6763+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:368
6764msgid "Pause"
6765msgstr ""
67666767+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:319
6768msgid "Pause video"
6769msgstr ""
6770···68716872#: src/components/Post/Embed/ExternalEmbed/Gif.tsx:44
6873#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:137
6874+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:369
6875msgid "Play"
6876msgstr ""
6877···6880msgstr ""
68816882#: src/components/Post/Embed/VideoEmbed/index.tsx:115
6883+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:320
6884msgid "Play video"
6885msgstr ""
6886···7065msgid "Porn"
7066msgstr ""
70677068+#: src/screens/PostThread/index.tsx:531
7069msgctxt "description"
7070msgid "Post"
7071msgstr ""
···7536msgid "Remove attachment"
7537msgstr ""
75387539+#: src/view/com/util/UserAvatar.tsx:498
7540+#: src/view/com/util/UserAvatar.tsx:501
7541msgid "Remove Avatar"
7542msgstr ""
7543···7608msgid "Remove repost"
7609msgstr ""
76107611+#: src/view/com/composer/videos/SubtitleDialog.tsx:282
7612msgid "Remove subtitle file"
7613msgstr ""
7614···76277628#: src/components/verification/VerificationRemovePrompt.tsx:46
7629#: src/components/verification/VerificationsDialog.tsx:252
7630+#: src/view/com/profile/ProfileMenu.tsx:408
7631+#: src/view/com/profile/ProfileMenu.tsx:411
7632msgid "Remove verification"
7633msgstr ""
7634···7763msgid "Report"
7764msgstr ""
77657766+#: src/view/com/profile/ProfileMenu.tsx:475
7767+#: src/view/com/profile/ProfileMenu.tsx:478
7768msgid "Report account"
7769msgstr ""
7770···8074msgid "Save changes"
8075msgstr ""
80768077+#: src/components/StarterPack/ShareDialog.tsx:142
8078+#: src/components/StarterPack/ShareDialog.tsx:148
8079msgid "Save image"
8080msgstr ""
8081···8223msgid "Search my posts"
8224msgstr ""
82258226+#: src/view/com/profile/ProfileMenu.tsx:291
8227+#: src/view/com/profile/ProfileMenu.tsx:294
8228msgid "Search posts"
8229msgstr ""
8230···8350msgid "Select content languages"
8351msgstr ""
83528353+#: src/components/live/GoLiveDialog.tsx:178
8354msgid "Select duration"
8355msgstr ""
8356···8384msgid "Select language"
8385msgstr ""
83868387+#: src/view/com/composer/videos/SubtitleDialog.tsx:269
8388msgid "Select language..."
8389msgstr ""
8390···8642msgid "Share a fun fact!"
8643msgstr ""
86448645+#: src/view/com/profile/ProfileMenu.tsx:562
8646msgid "Share anyway"
8647msgstr ""
8648···8654#: src/components/dialogs/LinkWarning.tsx:96
8655#: src/components/dialogs/LinkWarning.tsx:104
8656#: src/components/StarterPack/ShareDialog.tsx:113
8657+#: src/components/StarterPack/ShareDialog.tsx:122
8658msgid "Share link"
8659msgstr ""
8660···8667msgid "Share post at:// URI"
8668msgstr ""
86698670+#: src/components/StarterPack/ShareDialog.tsx:127
8671+#: src/components/StarterPack/ShareDialog.tsx:137
8672msgid "Share QR code"
8673msgstr ""
8674···8974msgid "Someone reacted {0}"
8975msgstr ""
89768977+#: src/screens/Messages/components/ChatListItem.tsx:243
8978msgid "Someone reacted {0} to {1}"
8979msgstr ""
8980···94049405#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:186
9406#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:391
9407+#: src/view/com/profile/ProfileMenu.tsx:538
9408msgid "The account will be able to interact with you after unblocking."
9409msgstr ""
9410···9442msgid "The Discover feed now knows what you like"
9443msgstr ""
94449445+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:332
9446msgid "The experience is better in the app. Download Bluesky now and we'll pick back up where you left off."
9447msgstr ""
9448···9458msgid "The following labels were applied to your content."
9459msgstr ""
94609461+#: src/components/live/GoLiveDialog.tsx:160
9462msgid "The following services are enabled for your account: {allowedServices}"
9463msgstr ""
9464···9551msgstr ""
95529553#: src/screens/Search/Explore.tsx:999
9554+#: src/view/com/posts/PostFeed.tsx:758
9555msgid "There was an issue fetching posts. Tap here to try again."
9556msgstr ""
9557···9702msgid "This content is not viewable without a Bluesky account."
9703msgstr ""
97049705+#: src/screens/Messages/components/ChatListItem.tsx:365
9706msgid "This conversation is with a deleted or a deactivated account. Press for options"
9707msgstr ""
9708···9757msgstr ""
97589759#: src/components/live/EditLiveDialog.tsx:176
9760+#: src/components/live/GoLiveDialog.tsx:153
9761msgid "This is not a valid link"
9762msgstr ""
9763···9817msgid "This post's author has disabled quote posts."
9818msgstr ""
98199820+#: src/view/com/profile/ProfileMenu.tsx:559
9821msgid "This profile is only visible to logged-in users. It won't be visible to people who aren't signed in."
9822msgstr ""
9823···10090#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:394
10091#: src/screens/ProfileList/components/Header.tsx:171
10092#: src/screens/ProfileList/components/Header.tsx:178
10093+#: src/view/com/profile/ProfileMenu.tsx:550
10094msgid "Unblock"
10095msgstr ""
10096···1010110102#: src/components/dms/ConvoMenu.tsx:261
10103#: src/components/dms/ConvoMenu.tsx:264
10104+#: src/view/com/profile/ProfileMenu.tsx:455
10105+#: src/view/com/profile/ProfileMenu.tsx:461
10106msgid "Unblock account"
10107msgstr ""
1010810109#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:184
10110#: src/screens/Profile/Header/ProfileHeaderStandard.tsx:389
10111+#: src/view/com/profile/ProfileMenu.tsx:532
10112msgid "Unblock Account?"
10113msgstr ""
10114···10141msgid "Unfollow {0}"
10142msgstr ""
1014310144+#: src/view/com/profile/ProfileMenu.tsx:311
10145+#: src/view/com/profile/ProfileMenu.tsx:321
10146msgid "Unfollow account"
10147msgstr ""
10148···10184msgstr ""
1018510186#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx:152
10187+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VolumeControl.tsx:96
10188msgctxt "video"
10189msgid "Unmute"
10190msgstr ""
···1020110202#: src/components/PostControls/PostMenu/PostMenuItems.tsx:685
10203#: src/components/PostControls/PostMenu/PostMenuItems.tsx:691
10204+#: src/view/com/profile/ProfileMenu.tsx:434
10205+#: src/view/com/profile/ProfileMenu.tsx:440
10206msgid "Unmute account"
10207msgstr ""
10208···10220msgid "Unmute thread"
10221msgstr ""
1022210223+#: src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx:317
10224msgid "Unmute video"
10225msgstr ""
10226···10339msgid "Upload a text file to:"
10340msgstr ""
1034110342+#: src/view/com/util/UserAvatar.tsx:469
10343+#: src/view/com/util/UserAvatar.tsx:472
10344#: src/view/com/util/UserBanner.tsx:159
10345#: src/view/com/util/UserBanner.tsx:162
10346msgid "Upload from Camera"
10347msgstr ""
1034810349+#: src/view/com/util/UserAvatar.tsx:486
10350#: src/view/com/util/UserBanner.tsx:176
10351msgid "Upload from Files"
10352msgstr ""
1035310354+#: src/view/com/util/UserAvatar.tsx:480
10355+#: src/view/com/util/UserAvatar.tsx:484
10356#: src/view/com/util/UserBanner.tsx:170
10357#: src/view/com/util/UserBanner.tsx:174
10358msgid "Upload from Library"
···1051010511#: src/components/verification/VerificationCreatePrompt.tsx:84
10512#: src/components/verification/VerificationCreatePrompt.tsx:86
10513+#: src/view/com/profile/ProfileMenu.tsx:418
10514+#: src/view/com/profile/ProfileMenu.tsx:421
10515msgid "Verify account"
10516msgstr ""
10517···10626msgid "Video not found."
10627msgstr ""
1062810629+#: src/view/com/composer/videos/SubtitleDialog.tsx:106
10630msgid "Video settings"
10631msgstr ""
10632···11120msgstr ""
1112111122#: src/components/live/EditLiveDialog.tsx:152
11123+#: src/components/live/GoLiveDialog.tsx:134
11124msgid "www.mylivestream.tv"
11125msgstr ""
11126···11464msgid "You reacted {0}"
11465msgstr ""
1146611467+#: src/screens/Messages/components/ChatListItem.tsx:220
11468msgid "You reacted {0} to {1}"
11469msgstr ""
11470···11494msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password."
11495msgstr ""
1149611497+#: src/screens/Messages/components/ChatListItem.tsx:156
11498msgid "You: {0}"
11499msgstr ""
1150011501+#: src/screens/Messages/components/ChatListItem.tsx:185
11502msgid "You: {defaultEmbeddedContentMessage}"
11503msgstr ""
1150411505+#: src/screens/Messages/components/ChatListItem.tsx:178
11506msgid "You: {short}"
11507msgstr ""
11508···11514msgid "You'll follow the suggested users once you finish creating your account!"
11515msgstr ""
1151611517+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:244
11518msgid "You'll follow these people and {0} others"
11519msgstr ""
1152011521+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:242
11522msgid "You'll follow these people right away"
11523msgstr ""
11524···11530msgid "You'll start receiving notifications for {0}!"
11531msgstr ""
1153211533+#: src/screens/StarterPack/StarterPackLandingScreen.tsx:282
11534msgid "You'll stay updated with these feeds"
11535msgstr ""
11536
+2-2
src/logger/index.ts
···11 type Transport,
12} from '#/logger/types'
13import {enabledLogLevels} from '#/logger/util'
14-import {isNative} from '#/platform/detection'
15import {ENV} from '#/env'
16import {bitdriftTransport} from './transports/bitdrift'
17import {sentryTransport} from './transports/sentry'
···21const TRANSPORTS: Transport[] = (function configureTransports() {
22 switch (ENV) {
23 case 'production': {
24- return [sentryTransport, isNative && bitdriftTransport].filter(
25 Boolean,
26 ) as Transport[]
27 }
···11 type Transport,
12} from '#/logger/types'
13import {enabledLogLevels} from '#/logger/util'
14+import {IS_NATIVE} from '#/env'
15import {ENV} from '#/env'
16import {bitdriftTransport} from './transports/bitdrift'
17import {sentryTransport} from './transports/sentry'
···21const TRANSPORTS: Transport[] = (function configureTransports() {
22 switch (ENV) {
23 case 'production': {
24+ return [sentryTransport, IS_NATIVE && bitdriftTransport].filter(
25 Boolean,
26 ) as Transport[]
27 }
+2-2
src/logger/transports/console.ts
···23import {LogLevel, type Transport} from '#/logger/types'
4import {prepareMetadata} from '#/logger/util'
5-import {isWeb} from '#/platform/detection'
67/**
8 * Used in dev mode to nicely log to the console
···33 msg += ` ${message.toString()}`
34 }
3536- if (isWeb) {
37 if (hasMetadata) {
38 console.groupCollapsed(msg)
39 console.log(metadata)
···23import {LogLevel, type Transport} from '#/logger/types'
4import {prepareMetadata} from '#/logger/util'
5+import {IS_WEB} from '#/env'
67/**
8 * Used in dev mode to nicely log to the console
···33 msg += ` ${message.toString()}`
34 }
3536+ if (IS_WEB) {
37 if (hasMetadata) {
38 console.groupCollapsed(msg)
39 console.log(metadata)
···21 type NativeStackScreenProps,
22} from '#/lib/routes/types'
23import {logger} from '#/logger'
24-import {isIOS} from '#/platform/detection'
25import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
26import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
27import {useSetMinimalShellMode} from '#/state/shell'
···38import * as Skele from '#/components/Skeleton'
39import * as toast from '#/components/Toast'
40import {Text} from '#/components/Typography'
04142type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
43···193 }
194 initialNumToRender={initialNumToRender}
195 windowSize={9}
196- maxToRenderPerBatch={isIOS ? 5 : 1}
197 updateCellsBatchingPeriod={40}
198 sideBorders={false}
199 />
···21 type NativeStackScreenProps,
22} from '#/lib/routes/types'
23import {logger} from '#/logger'
024import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
25import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
26import {useSetMinimalShellMode} from '#/state/shell'
···37import * as Skele from '#/components/Skeleton'
38import * as toast from '#/components/Toast'
39import {Text} from '#/components/Typography'
40+import {IS_IOS} from '#/env'
4142type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
43···193 }
194 initialNumToRender={initialNumToRender}
195 windowSize={9}
196+ maxToRenderPerBatch={IS_IOS ? 5 : 1}
197 updateCellsBatchingPeriod={40}
198 sideBorders={false}
199 />
+4-4
src/screens/Deactivated.tsx
···78import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
9import {logger} from '#/logger'
10-import {isWeb} from '#/platform/detection'
11import {
12 type SessionAccount,
13 useAgent,
···25import * as Layout from '#/components/Layout'
26import {Loader} from '#/components/Loader'
27import {Text} from '#/components/Typography'
02829const COL_WIDTH = 400
30···56 }, [setShowLoggedOut])
5758 const onPressLogout = React.useCallback(() => {
59- if (isWeb) {
60 // We're switching accounts, which remounts the entire app.
61 // On mobile, this gets us Home, but on the web we also need reset the URL.
62 // We can't change the URL via a navigate() call because the navigator
···102 contentContainerStyle={[
103 a.px_2xl,
104 {
105- paddingTop: isWeb ? 64 : insets.top + 16,
106- paddingBottom: isWeb ? 64 : insets.bottom,
107 },
108 ]}>
109 <View
···78import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
9import {logger} from '#/logger'
010import {
11 type SessionAccount,
12 useAgent,
···24import * as Layout from '#/components/Layout'
25import {Loader} from '#/components/Loader'
26import {Text} from '#/components/Typography'
27+import {IS_WEB} from '#/env'
2829const COL_WIDTH = 400
30···56 }, [setShowLoggedOut])
5758 const onPressLogout = React.useCallback(() => {
59+ if (IS_WEB) {
60 // We're switching accounts, which remounts the entire app.
61 // On mobile, this gets us Home, but on the web we also need reset the URL.
62 // We can't change the URL via a navigate() call because the navigator
···102 contentContainerStyle={[
103 a.px_2xl,
104 {
105+ paddingTop: IS_WEB ? 64 : insets.top + 16,
106+ paddingBottom: IS_WEB ? 64 : insets.bottom,
107 },
108 ]}>
109 <View
+2-2
src/screens/FindContactsFlowScreen.tsx
···9 type AllNavigatorParams,
10 type NativeStackScreenProps,
11} from '#/lib/routes/types'
12-import {isNative} from '#/platform/detection'
13import {useSetMinimalShellMode} from '#/state/shell'
14import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
15import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
16import {useFindContactsFlowState} from '#/components/contacts/state'
17import * as Layout from '#/components/Layout'
18import {ScreenTransition} from '#/components/ScreenTransition'
01920type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'>
21export function FindContactsFlowScreen({navigation}: Props) {
···4849 return (
50 <Layout.Screen>
51- {isNative ? (
52 <LayoutAnimationConfig skipEntering skipExiting>
53 <ScreenTransition key={state.step} direction={transitionDirection}>
54 <FindContactsFlow
···9 type AllNavigatorParams,
10 type NativeStackScreenProps,
11} from '#/lib/routes/types'
012import {useSetMinimalShellMode} from '#/state/shell'
13import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
14import {FindContactsFlow} from '#/components/contacts/FindContactsFlow'
15import {useFindContactsFlowState} from '#/components/contacts/state'
16import * as Layout from '#/components/Layout'
17import {ScreenTransition} from '#/components/ScreenTransition'
18+import {IS_NATIVE} from '#/env'
1920type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsFlow'>
21export function FindContactsFlowScreen({navigation}: Props) {
···4849 return (
50 <Layout.Screen>
51+ {IS_NATIVE ? (
52 <LayoutAnimationConfig skipEntering skipExiting>
53 <ScreenTransition key={state.step} direction={transitionDirection}>
54 <FindContactsFlow
+2-2
src/screens/Login/LoginForm.tsx
···18import {cleanError} from '#/lib/strings/errors'
19import {createFullHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
21-import {isIOS} from '#/platform/detection'
22import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
23import {useSessionApi} from '#/state/session'
24import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···32import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
33import {Loader} from '#/components/Loader'
34import {Text} from '#/components/Typography'
035import {FormContainer} from './FormContainer'
3637type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
···217 inputRef={identifierRef}
218 label={_(msg`Username or email address`)}
219 autoCapitalize="none"
220- autoFocus={!isIOS}
221 autoCorrect={false}
222 autoComplete="username"
223 returnKeyType="next"
···18import {cleanError} from '#/lib/strings/errors'
19import {createFullHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
021import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
22import {useSessionApi} from '#/state/session'
23import {useLoggedOutViewControls} from '#/state/shell/logged-out'
···31import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
32import {Loader} from '#/components/Loader'
33import {Text} from '#/components/Typography'
34+import {IS_IOS} from '#/env'
35import {FormContainer} from './FormContainer'
3637type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
···217 inputRef={identifierRef}
218 label={_(msg`Username or email address`)}
219 autoCapitalize="none"
220+ autoFocus={!IS_IOS}
221 autoCorrect={false}
222 autoComplete="username"
223 returnKeyType="next"
+3-3
src/screens/Messages/ChatList.tsx
···13import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
14import {cleanError} from '#/lib/strings/errors'
15import {logger} from '#/logger'
16-import {isNative} from '#/platform/detection'
17import {listenSoftReset} from '#/state/events'
18import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
19import {useMessagesEventBus} from '#/state/messages/events'
···39import {Link} from '#/components/Link'
40import {ListFooter} from '#/components/Lists'
41import {Text} from '#/components/Typography'
042import {ChatListItem} from './components/ChatListItem'
43import {InboxPreview} from './components/InboxPreview'
44···223224 const onSoftReset = useCallback(async () => {
225 scrollElRef.current?.scrollToOffset({
226- animated: isNative,
227 offset: 0,
228 })
229 try {
···349 hasNextPage={hasNextPage}
350 />
351 }
352- onEndReachedThreshold={isNative ? 1.5 : 0}
353 initialNumToRender={initialNumToRender}
354 windowSize={11}
355 desktopFixedHeight
···13import {type MessagesTabNavigatorParams} from '#/lib/routes/types'
14import {cleanError} from '#/lib/strings/errors'
15import {logger} from '#/logger'
016import {listenSoftReset} from '#/state/events'
17import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
18import {useMessagesEventBus} from '#/state/messages/events'
···38import {Link} from '#/components/Link'
39import {ListFooter} from '#/components/Lists'
40import {Text} from '#/components/Typography'
41+import {IS_NATIVE} from '#/env'
42import {ChatListItem} from './components/ChatListItem'
43import {InboxPreview} from './components/InboxPreview'
44···223224 const onSoftReset = useCallback(async () => {
225 scrollElRef.current?.scrollToOffset({
226+ animated: IS_NATIVE,
227 offset: 0,
228 })
229 try {
···349 hasNextPage={hasNextPage}
350 />
351 }
352+ onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
353 initialNumToRender={initialNumToRender}
354 windowSize={11}
355 desktopFixedHeight
+2-2
src/screens/Messages/Conversation.tsx
···21 type CommonNavigatorParams,
22 type NavigationProp,
23} from '#/lib/routes/types'
24-import {isWeb} from '#/platform/detection'
25import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
26import {useEmail} from '#/state/email-verification'
27import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
···43import {Error} from '#/components/Error'
44import * as Layout from '#/components/Layout'
45import {Loader} from '#/components/Loader'
04647type Props = NativeStackScreenProps<
48 CommonNavigatorParams,
···74 useCallback(() => {
75 setCurrentConvoId(convoId)
7677- if (isWeb && !gtMobile) {
78 setMinimalShellMode(true)
79 } else {
80 setMinimalShellMode(false)
···21 type CommonNavigatorParams,
22 type NavigationProp,
23} from '#/lib/routes/types'
024import {type Shadow, useMaybeProfileShadow} from '#/state/cache/profile-shadow'
25import {useEmail} from '#/state/email-verification'
26import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
···42import {Error} from '#/components/Error'
43import * as Layout from '#/components/Layout'
44import {Loader} from '#/components/Loader'
45+import {IS_WEB} from '#/env'
4647type Props = NativeStackScreenProps<
48 CommonNavigatorParams,
···74 useCallback(() => {
75 setCurrentConvoId(convoId)
7677+ if (IS_WEB && !gtMobile) {
78 setMinimalShellMode(true)
79 } else {
80 setMinimalShellMode(false)
+2-2
src/screens/Messages/Inbox.tsx
···21} from '#/lib/routes/types'
22import {cleanError} from '#/lib/strings/errors'
23import {logger} from '#/logger'
24-import {isNative} from '#/platform/detection'
25import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
26import {useMessagesEventBus} from '#/state/messages/events'
27import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
···44import * as Layout from '#/components/Layout'
45import {ListFooter} from '#/components/Lists'
46import {Text} from '#/components/Typography'
047import {RequestListItem} from './components/RequestListItem'
4849type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
···288 hasNextPage={hasNextPage}
289 />
290 }
291- onEndReachedThreshold={isNative ? 1.5 : 0}
292 initialNumToRender={initialNumToRender}
293 windowSize={11}
294 desktopFixedHeight
···21} from '#/lib/routes/types'
22import {cleanError} from '#/lib/strings/errors'
23import {logger} from '#/logger'
024import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
25import {useMessagesEventBus} from '#/state/messages/events'
26import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
···43import * as Layout from '#/components/Layout'
44import {ListFooter} from '#/components/Lists'
45import {Text} from '#/components/Typography'
46+import {IS_NATIVE} from '#/env'
47import {RequestListItem} from './components/RequestListItem'
4849type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesInbox'>
···288 hasNextPage={hasNextPage}
289 />
290 }
291+ onEndReachedThreshold={IS_NATIVE ? 1.5 : 0}
292 initialNumToRender={initialNumToRender}
293 windowSize={11}
294 desktopFixedHeight
+2-2
src/screens/Messages/Settings.tsx
···5import {type NativeStackScreenProps} from '@react-navigation/native-stack'
67import {type CommonNavigatorParams} from '#/lib/routes/types'
8-import {isNative} from '#/platform/detection'
9import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
10import {useProfileQuery} from '#/state/queries/profile'
11import {useSession} from '#/state/session'
···16import * as Toggle from '#/components/forms/Toggle'
17import * as Layout from '#/components/Layout'
18import {Text} from '#/components/Typography'
019import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
2021type AllowIncoming = 'all' | 'none' | 'following'
···118 you choose.
119 </Trans>
120 </Admonition>
121- {isNative && (
122 <>
123 <Divider style={a.my_md} />
124 <Text style={[a.text_lg, a.font_semi_bold]}>
···5import {type NativeStackScreenProps} from '@react-navigation/native-stack'
67import {type CommonNavigatorParams} from '#/lib/routes/types'
08import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
9import {useProfileQuery} from '#/state/queries/profile'
10import {useSession} from '#/state/session'
···15import * as Toggle from '#/components/forms/Toggle'
16import * as Layout from '#/components/Layout'
17import {Text} from '#/components/Typography'
18+import {IS_NATIVE} from '#/env'
19import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
2021type AllowIncoming = 'all' | 'none' | 'following'
···118 you choose.
119 </Trans>
120 </Admonition>
121+ {IS_NATIVE && (
122 <>
123 <Divider style={a.my_md} />
124 <Text style={[a.text_lg, a.font_semi_bold]}>
+6-7
src/screens/Messages/components/ChatListItem.tsx
···20 toBskyAppUrl,
21 toShortUrl,
22} from '#/lib/strings/url-helpers'
23-import {isNative} from '#/platform/detection'
24import {useProfileShadow} from '#/state/cache/profile-shadow'
25import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
26import {useModerationOpts} from '#/state/preferences/moderation-opts'
···47import {Text} from '#/components/Typography'
48import {useSimpleVerificationState} from '#/components/verification'
49import {VerificationCheck} from '#/components/verification/VerificationCheck'
050import type * as bsky from '#/types/bsky'
5152export const ChatListItemPortal = createPortalGroup()
···142143 const {lastMessage, lastMessageSentAt, latestReportableMessage} =
144 useMemo(() => {
145- // eslint-disable-next-line @typescript-eslint/no-shadow
146 let lastMessage = _(msg`No messages yet`)
147- // eslint-disable-next-line @typescript-eslint/no-shadow
148 let lastMessageSentAt: string | null = null
149- // eslint-disable-next-line @typescript-eslint/no-shadow
150 let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
151152 if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
···371 )
372 }
373 accessibilityActions={
374- isNative
375 ? [
376 {
377 name: 'magicTap',
···385 : undefined
386 }
387 onPress={onPress}
388- onLongPress={isNative ? onLongPress : undefined}
389 onAccessibilityAction={onLongPress}>
390 {({hovered, pressed, focused}) => (
391 <View
···524 control={menuControl}
525 currentScreen="list"
526 showMarkAsRead={convo.unreadCount > 0}
527- hideTrigger={isNative}
528 blockInfo={blockInfo}
529 style={[
530 a.absolute,
···20 toBskyAppUrl,
21 toShortUrl,
22} from '#/lib/strings/url-helpers'
023import {useProfileShadow} from '#/state/cache/profile-shadow'
24import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
25import {useModerationOpts} from '#/state/preferences/moderation-opts'
···46import {Text} from '#/components/Typography'
47import {useSimpleVerificationState} from '#/components/verification'
48import {VerificationCheck} from '#/components/verification/VerificationCheck'
49+import {IS_NATIVE} from '#/env'
50import type * as bsky from '#/types/bsky'
5152export const ChatListItemPortal = createPortalGroup()
···142143 const {lastMessage, lastMessageSentAt, latestReportableMessage} =
144 useMemo(() => {
0145 let lastMessage = _(msg`No messages yet`)
146+147 let lastMessageSentAt: string | null = null
148+149 let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined
150151 if (ChatBskyConvoDefs.isMessageView(convo.lastMessage)) {
···370 )
371 }
372 accessibilityActions={
373+ IS_NATIVE
374 ? [
375 {
376 name: 'magicTap',
···384 : undefined
385 }
386 onPress={onPress}
387+ onLongPress={IS_NATIVE ? onLongPress : undefined}
388 onAccessibilityAction={onLongPress}>
389 {({hovered, pressed, focused}) => (
390 <View
···523 control={menuControl}
524 currentScreen="list"
525 showMarkAsRead={convo.unreadCount > 0}
526+ hideTrigger={IS_NATIVE}
527 blockInfo={blockInfo}
528 style={[
529 a.absolute,
+5-5
src/screens/Messages/components/MessageInput.tsx
···1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
21-import {isIOS, isWeb} from '#/platform/detection'
22import {useEmail} from '#/state/email-verification'
23import {
24 useMessageDraft,
···30import {android, atoms as a, useTheme} from '#/alf'
31import {useSharedInputStyles} from '#/components/forms/TextField'
32import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
033import {useExtractEmbedFromFacets} from './MessageInputEmbed'
3435const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
···87 playHaptic()
88 setEmbed(undefined)
89 setMessage('')
90- if (isIOS) {
91 setShouldEnforceClear(true)
92 }
93- if (isWeb) {
94 // Pressing the send button causes the text input to lose focus, so we need to
95 // re-focus it after sending
96 setTimeout(() => {
···163 // next change and double make sure the input is cleared. It should *always* send an onChange event after
164 // clearing via setMessage('') that happens in onSubmit()
165 // -sfn
166- if (isIOS && shouldEnforceClear) {
167 setShouldEnforceClear(false)
168 setMessage('')
169 return
···178 a.px_sm,
179 t.atoms.text,
180 android({paddingTop: 0}),
181- {paddingBottom: isIOS ? 5 : 0},
182 animatedStyle,
183 ]}
184 keyboardAppearance={t.scheme}
···1819import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
20import {useHaptics} from '#/lib/haptics'
021import {useEmail} from '#/state/email-verification'
22import {
23 useMessageDraft,
···29import {android, atoms as a, useTheme} from '#/alf'
30import {useSharedInputStyles} from '#/components/forms/TextField'
31import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
32+import {IS_IOS, IS_WEB} from '#/env'
33import {useExtractEmbedFromFacets} from './MessageInputEmbed'
3435const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)
···87 playHaptic()
88 setEmbed(undefined)
89 setMessage('')
90+ if (IS_IOS) {
91 setShouldEnforceClear(true)
92 }
93+ if (IS_WEB) {
94 // Pressing the send button causes the text input to lose focus, so we need to
95 // re-focus it after sending
96 setTimeout(() => {
···163 // next change and double make sure the input is cleared. It should *always* send an onChange event after
164 // clearing via setMessage('') that happens in onSubmit()
165 // -sfn
166+ if (IS_IOS && shouldEnforceClear) {
167 setShouldEnforceClear(false)
168 setMessage('')
169 return
···178 a.px_sm,
179 t.atoms.text,
180 android({paddingTop: 0}),
181+ {paddingBottom: IS_IOS ? 5 : 0},
182 animatedStyle,
183 ]}
184 keyboardAppearance={t.scheme}
···6import TextareaAutosize from 'react-textarea-autosize'
7import {countGraphemes} from 'unicode-segmenter/grapheme'
89-import {isSafari, isTouchDevice} from '#/lib/browser'
10import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
11import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
12import {
···25import {useSharedInputStyles} from '#/components/forms/TextField'
26import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
27import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
028import {useExtractEmbedFromFacets} from './MessageInputEmbed'
2930export function MessageInput({
···88 // far too long of a delay, and a subsequent enter press would often just end up doing nothing. A shorter time
89 // frame was also not great, since it was too short to be reliable (i.e. an older system might have a larger
90 // time gap between the two events firing.
91- if (isSafari && e.key === 'Enter' && e.keyCode === 229) {
92 return
93 }
94···231 onChange={onChange}
232 // On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message
233 // in these cases.
234- onKeyDown={isTouchDevice && isMobile ? undefined : onKeyDown}
235 />
236 <Pressable
237 accessibilityRole="button"
···6import TextareaAutosize from 'react-textarea-autosize'
7import {countGraphemes} from 'unicode-segmenter/grapheme'
809import {MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants'
10import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
11import {
···24import {useSharedInputStyles} from '#/components/forms/TextField'
25import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
26import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
27+import {IS_WEB_SAFARI, IS_WEB_TOUCH_DEVICE} from '#/env'
28import {useExtractEmbedFromFacets} from './MessageInputEmbed'
2930export function MessageInput({
···88 // far too long of a delay, and a subsequent enter press would often just end up doing nothing. A shorter time
89 // frame was also not great, since it was too short to be reliable (i.e. an older system might have a larger
90 // time gap between the two events firing.
91+ if (IS_WEB_SAFARI && e.key === 'Enter' && e.keyCode === 229) {
92 return
93 }
94···231 onChange={onChange}
232 // On mobile web phones, we want to keep the same behavior as the native app. Do not submit the message
233 // in these cases.
234+ onKeyDown={IS_WEB_TOUCH_DEVICE && isMobile ? undefined : onKeyDown}
235 />
236 <Pressable
237 accessibilityRole="button"
+7-7
src/screens/Messages/components/MessagesList.tsx
···24 isBskyPostUrl,
25} from '#/lib/strings/url-helpers'
26import {logger} from '#/logger'
27-import {isNative} from '#/platform/detection'
28-import {isWeb} from '#/platform/detection'
29import {
30 type ActiveConvoStates,
31 isConvoActive,
···52import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
53import {Loader} from '#/components/Loader'
54import {Text} from '#/components/Typography'
0055import {ChatStatusInfo} from './ChatStatusInfo'
56import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
57···159 (_: number, height: number) => {
160 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
161 // previous off whenever we add new content to the previous offset whenever we add new content to the list.
162- if (isWeb && isAtTop.get() && hasScrolled) {
163 flatListRef.current?.scrollToOffset({
164 offset: height - prevContentHeight.current,
165 animated: false,
···399 (e: LayoutChangeEvent) => {
400 layoutHeight.set(e.nativeEvent.layout.height)
401402- if (isWeb || !keyboardIsOpening.get()) {
403 flatListRef.current?.scrollToEnd({
404 animated: !layoutScrollWithoutAnimation.get(),
405 })
···438 disableVirtualization={true}
439 style={animatedListStyle}
440 // The extra two items account for the header and the footer components
441- initialNumToRender={isNative ? 32 : 62}
442- maxToRenderPerBatch={isWeb ? 32 : 62}
443 keyboardDismissMode="on-drag"
444 keyboardShouldPersistTaps="handled"
445 maintainVisibleContentPosition={{
···477 )}
478 </Animated.View>
479480- {isWeb && (
481 <EmojiPicker
482 pinToTop
483 state={emojiPickerState}
···24 isBskyPostUrl,
25} from '#/lib/strings/url-helpers'
26import {logger} from '#/logger'
0027import {
28 type ActiveConvoStates,
29 isConvoActive,
···50import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
51import {Loader} from '#/components/Loader'
52import {Text} from '#/components/Typography'
53+import {IS_NATIVE} from '#/env'
54+import {IS_WEB} from '#/env'
55import {ChatStatusInfo} from './ChatStatusInfo'
56import {MessageInputEmbed, useMessageEmbed} from './MessageInputEmbed'
57···159 (_: number, height: number) => {
160 // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the
161 // previous off whenever we add new content to the previous offset whenever we add new content to the list.
162+ if (IS_WEB && isAtTop.get() && hasScrolled) {
163 flatListRef.current?.scrollToOffset({
164 offset: height - prevContentHeight.current,
165 animated: false,
···399 (e: LayoutChangeEvent) => {
400 layoutHeight.set(e.nativeEvent.layout.height)
401402+ if (IS_WEB || !keyboardIsOpening.get()) {
403 flatListRef.current?.scrollToEnd({
404 animated: !layoutScrollWithoutAnimation.get(),
405 })
···438 disableVirtualization={true}
439 style={animatedListStyle}
440 // The extra two items account for the header and the footer components
441+ initialNumToRender={IS_NATIVE ? 32 : 62}
442+ maxToRenderPerBatch={IS_WEB ? 32 : 62}
443 keyboardDismissMode="on-drag"
444 keyboardShouldPersistTaps="handled"
445 maintainVisibleContentPosition={{
···477 )}
478 </Animated.View>
479480+ {IS_WEB && (
481 <EmojiPicker
482 pinToTop
483 state={emojiPickerState}
+2-2
src/screens/Moderation/index.tsx
···11 type NativeStackScreenProps,
12} from '#/lib/routes/types'
13import {logger} from '#/logger'
14-import {isIOS} from '#/platform/detection'
15import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
16import {
17 useMyLabelersQuery,
···45import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
46import {Text} from '#/components/Typography'
47import {useAgeAssurance} from '#/ageAssurance'
04849function ErrorState({error}: {error: string}) {
50 const t = useTheme()
···182 (optimisticAdultContent && optimisticAdultContent.enabled) ||
183 (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
184 )
185- const adultContentUIDisabledOnIOS = isIOS && !adultContentEnabled
186 let adultContentUIDisabled = adultContentUIDisabledOnIOS
187188 if (aa.flags.adultContentDisabled) {
···11 type NativeStackScreenProps,
12} from '#/lib/routes/types'
13import {logger} from '#/logger'
014import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
15import {
16 useMyLabelersQuery,
···44import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
45import {Text} from '#/components/Typography'
46import {useAgeAssurance} from '#/ageAssurance'
47+import {IS_IOS} from '#/env'
4849function ErrorState({error}: {error: string}) {
50 const t = useTheme()
···182 (optimisticAdultContent && optimisticAdultContent.enabled) ||
183 (!optimisticAdultContent && preferences.moderationPrefs.adultContentEnabled)
184 )
185+ const adultContentUIDisabledOnIOS = IS_IOS && !adultContentEnabled
186 let adultContentUIDisabled = adultContentUIDisabledOnIOS
187188 if (aa.flags.adultContentDisabled) {
···22import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
23import {logEvent, useGate} from '#/lib/statsig/statsig'
24import {logger} from '#/logger'
25-import {isWeb} from '#/platform/detection'
26import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
27import {getAllListMembers} from '#/state/queries/list-members'
28import {preferencesQueryKey} from '#/state/queries/preferences'
···47import {Button, ButtonIcon, ButtonText} from '#/components/Button'
48import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
49import {Loader} from '#/components/Loader'
050import * as bsky from '#/types/bsky'
51import {ValuePropositionPager} from './ValuePropositionPager'
52···305306 <OnboardingControls.Portal>
307 <View style={gtMobile && [a.gap_md, a.flex_row]}>
308- {gtMobile && (isWeb ? subStep !== 2 : true) && (
309 <Button
310 disabled={saving}
311 color="secondary"
···22import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
23import {logEvent, useGate} from '#/lib/statsig/statsig'
24import {logger} from '#/logger'
025import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
26import {getAllListMembers} from '#/state/queries/list-members'
27import {preferencesQueryKey} from '#/state/queries/preferences'
···46import {Button, ButtonIcon, ButtonText} from '#/components/Button'
47import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
48import {Loader} from '#/components/Loader'
49+import {IS_WEB} from '#/env'
50import * as bsky from '#/types/bsky'
51import {ValuePropositionPager} from './ValuePropositionPager'
52···305306 <OnboardingControls.Portal>
307 <View style={gtMobile && [a.gap_md, a.flex_row]}>
308+ {gtMobile && (IS_WEB ? subStep !== 2 : true) && (
309 <Button
310 disabled={saving}
311 color="secondary"
+3-3
src/screens/Onboarding/StepProfile/index.tsx
···17import {logEvent, useGate} from '#/lib/statsig/statsig'
18import {isCancelledError} from '#/lib/strings/errors'
19import {logger} from '#/logger'
20-import {isNative, isWeb} from '#/platform/detection'
21import {
22 OnboardingControls,
23 OnboardingDescriptionText,
···38import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
39import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
40import {Text} from '#/components/Typography'
041import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
4243export interface Avatar {
···183 let image = items[0]
184 if (!image) return
185186- if (!isWeb) {
187 try {
188 image = await openCropper({
189 imageUri: image.path,
···200201 // If we are on mobile, prefetching the image will load the image into memory before we try and display it,
202 // stopping any brief flickers.
203- if (isNative) {
204 await ExpoImage.prefetch(image.path)
205 }
206
···17import {logEvent, useGate} from '#/lib/statsig/statsig'
18import {isCancelledError} from '#/lib/strings/errors'
19import {logger} from '#/logger'
020import {
21 OnboardingControls,
22 OnboardingDescriptionText,
···37import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
38import {CircleInfo_Stroke2_Corner0_Rounded} from '#/components/icons/CircleInfo'
39import {Text} from '#/components/Typography'
40+import {IS_NATIVE, IS_WEB} from '#/env'
41import {type AvatarColor, avatarColors, type Emoji, emojiItems} from './types'
4243export interface Avatar {
···183 let image = items[0]
184 if (!image) return
185186+ if (!IS_WEB) {
187 try {
188 image = await openCropper({
189 imageUri: image.path,
···200201 // If we are on mobile, prefetching the image will load the image into memory before we try and display it,
202 // stopping any brief flickers.
203+ if (IS_NATIVE) {
204 await ExpoImage.prefetch(image.path)
205 }
206
···10import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
11import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
12import {logger} from '#/logger'
13-import {isWeb} from '#/platform/detection'
14import {updateProfileShadow} from '#/state/cache/profile-shadow'
15import {useLanguagePrefs} from '#/state/preferences'
16import {useModerationOpts} from '#/state/preferences/moderation-opts'
···31import {Loader} from '#/components/Loader'
32import * as ProfileCard from '#/components/ProfileCard'
33import * as toast from '#/components/Toast'
034import type * as bsky from '#/types/bsky'
35import {bulkWriteFollows} from '../util'
36···161 style={[
162 a.overflow_hidden,
163 a.mt_sm,
164- isWeb
165 ? [a.max_w_full, web({minHeight: '100vh'})]
166 : {marginHorizontal: tokens.space.xl * -1},
167 a.flex_1,
···213 a.mt_md,
214 a.border_y,
215 t.atoms.border_contrast_low,
216- isWeb && [a.border_x, a.rounded_sm, a.overflow_hidden],
217 ]}>
218 {suggestedUsers?.actors.map((user, index) => (
219 <SuggestedProfileCard
···324 ...interestsDisplayNames,
325 }
326 }
327- gutterWidth={isWeb ? 0 : tokens.space.xl}
328 />
329 )
330}
···350 const node = cardRef.current
351 if (!node || hasTrackedRef.current) return
352353- if (isWeb && typeof IntersectionObserver !== 'undefined') {
354 const observer = new IntersectionObserver(
355 entries => {
356 if (entries[0]?.isIntersecting && !hasTrackedRef.current) {
···10import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
11import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
12import {logger} from '#/logger'
013import {updateProfileShadow} from '#/state/cache/profile-shadow'
14import {useLanguagePrefs} from '#/state/preferences'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
···30import {Loader} from '#/components/Loader'
31import * as ProfileCard from '#/components/ProfileCard'
32import * as toast from '#/components/Toast'
33+import {IS_WEB} from '#/env'
34import type * as bsky from '#/types/bsky'
35import {bulkWriteFollows} from '../util'
36···161 style={[
162 a.overflow_hidden,
163 a.mt_sm,
164+ IS_WEB
165 ? [a.max_w_full, web({minHeight: '100vh'})]
166 : {marginHorizontal: tokens.space.xl * -1},
167 a.flex_1,
···213 a.mt_md,
214 a.border_y,
215 t.atoms.border_contrast_low,
216+ IS_WEB && [a.border_x, a.rounded_sm, a.overflow_hidden],
217 ]}>
218 {suggestedUsers?.actors.map((user, index) => (
219 <SuggestedProfileCard
···324 ...interestsDisplayNames,
325 }
326 }
327+ gutterWidth={IS_WEB ? 0 : tokens.space.xl}
328 />
329 )
330}
···350 const node = cardRef.current
351 if (!node || hasTrackedRef.current) return
352353+ if (IS_WEB && typeof IntersectionObserver !== 'undefined') {
354 const observer = new IntersectionObserver(
355 entries => {
356 if (entries[0]?.isIntersecting && !hasTrackedRef.current) {
+2-2
src/screens/Onboarding/index.tsx
···45import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
6import {useGate} from '#/lib/statsig/statsig'
7-import {isNative} from '#/platform/detection'
8import {useLanguagePrefs} from '#/state/preferences'
9import {
10 Layout,
···24import {useFindContactsFlowState} from '#/components/contacts/state'
25import {Portal} from '#/components/Portal'
26import {ScreenTransition} from '#/components/ScreenTransition'
027import {ENV} from '#/env'
28import {StepFindContacts} from './StepFindContacts'
29import {StepFindContactsIntro} from './StepFindContactsIntro'
···50 useIsFindContactsFeatureEnabledBasedOnGeolocation()
51 const showFindContacts =
52 ENV !== 'e2e' &&
53- isNative &&
54 findContactsEnabled &&
55 !gate('disable_onboarding_find_contacts')
56
···45import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
6import {useGate} from '#/lib/statsig/statsig'
07import {useLanguagePrefs} from '#/state/preferences'
8import {
9 Layout,
···23import {useFindContactsFlowState} from '#/components/contacts/state'
24import {Portal} from '#/components/Portal'
25import {ScreenTransition} from '#/components/ScreenTransition'
26+import {IS_NATIVE} from '#/env'
27import {ENV} from '#/env'
28import {StepFindContacts} from './StepFindContacts'
29import {StepFindContactsIntro} from './StepFindContactsIntro'
···50 useIsFindContactsFeatureEnabledBasedOnGeolocation()
51 const showFindContacts =
52 ENV !== 'e2e' &&
53+ IS_NATIVE &&
54 findContactsEnabled &&
55 !gate('disable_onboarding_find_contacts')
56
+7-6
src/screens/PostThread/components/GrowthHack.tsx
···3import {PrivacySensitive} from 'expo-privacy-sensitive'
45import {useAppState} from '#/lib/hooks/useAppState'
6-import {isIOS} from '#/platform/detection'
7import {atoms as a, useTheme} from '#/alf'
8import {sizes as iconSizes} from '#/components/icons/common'
9import {Mark as Logo} from '#/components/icons/Logo'
01011const ICON_SIZE = 'xl' as const
12···2526 const appState = useAppState()
2728- if (!isIOS || appState !== 'active') return children
2930 return (
31 <View
···33 a.relative,
34 a.justify_center,
35 align === 'right' ? a.align_end : a.align_start,
36- width === undefined ? {opacity: 0} : {minWidth: width},
37 ]}>
38 <PrivacySensitive
39 style={[
···46 // when finding the size of the button, we need the containing
47 // element to have a concrete size otherwise the text will
48 // collapse to 0 width. so set it to a really big number
49- // and hide the entire thing (see above)
50- width === undefined && {width: 10000},
051 ]}>
52 <View
53 onLayout={evt => setWidth(evt.nativeEvent.layout.width)}
54 style={[
55 t.atoms.bg,
56- // make sure it covers the icon! the won't always be a button
57 {minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]},
58 ]}>
59 {children}
···3import {PrivacySensitive} from 'expo-privacy-sensitive'
45import {useAppState} from '#/lib/hooks/useAppState'
06import {atoms as a, useTheme} from '#/alf'
7import {sizes as iconSizes} from '#/components/icons/common'
8import {Mark as Logo} from '#/components/icons/Logo'
9+import {IS_IOS} from '#/env'
1011const ICON_SIZE = 'xl' as const
12···2526 const appState = useAppState()
2728+ if (!IS_IOS || appState !== 'active') return children
2930 return (
31 <View
···33 a.relative,
34 a.justify_center,
35 align === 'right' ? a.align_end : a.align_start,
36+ {minWidth: width ?? iconSizes[ICON_SIZE]},
37 ]}>
38 <PrivacySensitive
39 style={[
···46 // when finding the size of the button, we need the containing
47 // element to have a concrete size otherwise the text will
48 // collapse to 0 width. so set it to a really big number
49+ // and just use `pointer-events: box-none` so it doesn't interfere with the UI
50+ {width: 1000},
51+ a.pointer_events_box_none,
52 ]}>
53 <View
54 onLayout={evt => setWidth(evt.nativeEvent.layout.width)}
55 style={[
56 t.atoms.bg,
57+ // make sure it covers the icon! children might be undefined
58 {minWidth: iconSizes[ICON_SIZE], minHeight: iconSizes[ICON_SIZE]},
59 ]}>
60 {children}
···5import {useNavigation} from '@react-navigation/native'
67import {logger} from '#/logger'
8-import {isIOS} from '#/platform/detection'
9import {useProfileShadow} from '#/state/cache/profile-shadow'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {
···18import {Button, ButtonIcon, ButtonText} from '#/components/Button'
19import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
20import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
021import {GrowthHack} from './GrowthHack'
2223export function ThreadItemAnchorFollowButton({did}: {did: string}) {
24- if (isIOS) {
25 return (
26 <GrowthHack>
27 <ThreadItemAnchorFollowButtonInner did={did} />
···5import {useNavigation} from '@react-navigation/native'
67import {logger} from '#/logger'
08import {useProfileShadow} from '#/state/cache/profile-shadow'
9import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {
···17import {Button, ButtonIcon, ButtonText} from '#/components/Button'
18import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
19import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
20+import {IS_IOS} from '#/env'
21import {GrowthHack} from './GrowthHack'
2223export function ThreadItemAnchorFollowButton({did}: {did: string}) {
24+ if (IS_IOS) {
25 return (
26 <GrowthHack>
27 <ThreadItemAnchorFollowButtonInner did={did} />
···64 */
65 const thread = usePostThread({anchor: uri})
66 const {anchor, hasParents} = useMemo(() => {
067 let hasParents = false
68 for (const item of thread.data.items) {
69 if (item.type === 'threadPost' && item.depth === 0) {
+2-2
src/screens/Profile/Header/GrowableAvatar.tsx
···7} from 'react-native-reanimated'
8import type React from 'react'
910-import {isIOS} from '#/platform/detection'
11import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
01213export function GrowableAvatar({
14 children,
···20 const pagerContext = usePagerHeaderContext()
2122 // pagerContext should only be present on iOS, but better safe than sorry
23- if (!pagerContext || !isIOS) {
24 return <View style={style}>{children}</View>
25 }
26
···7} from 'react-native-reanimated'
8import type React from 'react'
9010import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
11+import {IS_IOS} from '#/env'
1213export function GrowableAvatar({
14 children,
···20 const pagerContext = usePagerHeaderContext()
2122 // pagerContext should only be present on iOS, but better safe than sorry
23+ if (!pagerContext || !IS_IOS) {
24 return <View style={style}>{children}</View>
25 }
26
+3-3
src/screens/Profile/Header/GrowableBanner.tsx
···15import {useIsFetching} from '@tanstack/react-query'
16import type React from 'react'
1718-import {isIOS} from '#/platform/detection'
19import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
20import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
21import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
22import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
23import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
24import {atoms as a} from '#/alf'
02526const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
27···39 const pagerContext = usePagerHeaderContext()
4041 // plain non-growable mode for Android/Web
42- if (!pagerContext || !isIOS) {
43 return (
44 <Pressable
45 onPress={onPress}
···164 style={[
165 a.absolute,
166 a.inset_0,
167- {top: topInset - (isIOS ? 15 : 0)},
168 a.justify_center,
169 a.align_center,
170 ]}>
···15import {useIsFetching} from '@tanstack/react-query'
16import type React from 'react'
17018import {RQKEY_ROOT as STARTERPACK_RQKEY_ROOT} from '#/state/queries/actor-starter-packs'
19import {RQKEY_ROOT as FEED_RQKEY_ROOT} from '#/state/queries/post-feed'
20import {RQKEY_ROOT as FEEDGEN_RQKEY_ROOT} from '#/state/queries/profile-feedgens'
21import {RQKEY_ROOT as LIST_RQKEY_ROOT} from '#/state/queries/profile-lists'
22import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
23import {atoms as a} from '#/alf'
24+import {IS_IOS} from '#/env'
2526const AnimatedBlurView = Animated.createAnimatedComponent(BlurView)
27···39 const pagerContext = usePagerHeaderContext()
4041 // plain non-growable mode for Android/Web
42+ if (!pagerContext || !IS_IOS) {
43 return (
44 <Pressable
45 onPress={onPress}
···164 style={[
165 a.absolute,
166 a.inset_0,
167+ {top: topInset - (IS_IOS ? 15 : 0)},
168 a.justify_center,
169 a.align_center,
170 ]}>
+3-3
src/screens/Profile/Header/Handle.tsx
···4import {useLingui} from '@lingui/react'
56import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
7-import {isIOS, isNative} from '#/platform/detection'
8import {type Shadow} from '#/state/cache/types'
9import {useShowLinkInHandle} from '#/state/preferences/show-link-in-handle.tsx'
10import {atoms as a, useTheme, web} from '#/alf'
11import {InlineLinkText} from '#/components/Link.tsx'
12import {NewskieDialog} from '#/components/NewskieDialog'
13import {Text} from '#/components/Typography'
01415export function ProfileHeaderHandle({
16 profile,
···28 profile.handle,
29 '@',
30 // forceLTR handled by CSS above on web
31- isNative,
32 )
33 return (
34 <View
35 style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]}
36- pointerEvents={disableTaps ? 'none' : isIOS ? 'auto' : 'box-none'}>
37 <NewskieDialog profile={profile} disabled={disableTaps} />
3839 <Text
···4import {useLingui} from '@lingui/react'
56import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles'
07import {type Shadow} from '#/state/cache/types'
8import {useShowLinkInHandle} from '#/state/preferences/show-link-in-handle.tsx'
9import {atoms as a, useTheme, web} from '#/alf'
10import {InlineLinkText} from '#/components/Link.tsx'
11import {NewskieDialog} from '#/components/NewskieDialog'
12import {Text} from '#/components/Typography'
13+import {IS_IOS, IS_NATIVE} from '#/env'
1415export function ProfileHeaderHandle({
16 profile,
···28 profile.handle,
29 '@',
30 // forceLTR handled by CSS above on web
31+ IS_NATIVE,
32 )
33 return (
34 <View
35 style={[a.flex_row, a.gap_sm, a.align_center, {maxWidth: '100%'}]}
36+ pointerEvents={disableTaps ? 'none' : IS_IOS ? 'auto' : 'box-none'}>
37 <NewskieDialog profile={profile} disabled={disableTaps} />
3839 <Text
···15import {sanitizeDisplayName} from '#/lib/strings/display-names'
16import {sanitizeHandle} from '#/lib/strings/handles'
17import {logger} from '#/logger'
18-import {isIOS} from '#/platform/detection'
19import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
20import {useDisableFollowedByMetrics} from '#/state/preferences/disable-followed-by-metrics'
21import {
···40import * as Toast from '#/components/Toast'
41import {Text} from '#/components/Typography'
42import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
043import {EditProfileDialog} from './EditProfileDialog'
44import {ProfileHeaderHandle} from './Handle'
45import {ProfileHeaderMetrics} from './Metrics'
···107 isPlaceholderProfile={isPlaceholderProfile}>
108 <View
109 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
110- pointerEvents={isIOS ? 'auto' : 'box-none'}>
111 <View
112 style={[
113 {paddingLeft: 90},
···118 a.pb_sm,
119 a.flex_wrap,
120 ]}
121- pointerEvents={isIOS ? 'auto' : 'box-none'}>
122 <HeaderStandardButtons
123 profile={profile}
124 moderation={moderation}
···15import {sanitizeDisplayName} from '#/lib/strings/display-names'
16import {sanitizeHandle} from '#/lib/strings/handles'
17import {logger} from '#/logger'
018import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
19import {useDisableFollowedByMetrics} from '#/state/preferences/disable-followed-by-metrics'
20import {
···39import * as Toast from '#/components/Toast'
40import {Text} from '#/components/Typography'
41import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
42+import {IS_IOS} from '#/env'
43import {EditProfileDialog} from './EditProfileDialog'
44import {ProfileHeaderHandle} from './Handle'
45import {ProfileHeaderMetrics} from './Metrics'
···107 isPlaceholderProfile={isPlaceholderProfile}>
108 <View
109 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
110+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
111 <View
112 style={[
113 {paddingLeft: 90},
···118 a.pb_sm,
119 a.flex_wrap,
120 ]}
121+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
122 <HeaderStandardButtons
123 profile={profile}
124 moderation={moderation}
+5-5
src/screens/Profile/Header/Shell.tsx
···19import {useHaptics} from '#/lib/haptics'
20import {type NavigationProp} from '#/lib/routes/types'
21import {logger} from '#/logger'
22-import {isIOS} from '#/platform/detection'
23import {type Shadow} from '#/state/cache/types'
24import {useLightboxControls} from '#/state/lightbox'
25import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
···41import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
42import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
43import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
044import {GrowableAvatar} from './GrowableAvatar'
45import {GrowableBanner} from './GrowableBanner'
46import {StatusBarShadow} from './StatusBarShadow'
···196 }, [profile.banner, moderation, _openLightboxBanner, bannerRef])
197198 return (
199- <View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}>
200 <View
201- pointerEvents={isIOS ? 'auto' : 'box-none'}
202 style={[a.relative, {height: 150}]}>
203 <StatusBarShadow />
204 <GrowableBanner
···285 a.px_lg,
286 a.pt_xs,
287 a.pb_sm,
288- isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
289 ]}
290 />
291 ) : (
···295 a.px_lg,
296 a.pt_xs,
297 a.pb_sm,
298- isIOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
299 ]}
300 />
301 ))}
···19import {useHaptics} from '#/lib/haptics'
20import {type NavigationProp} from '#/lib/routes/types'
21import {logger} from '#/logger'
022import {type Shadow} from '#/state/cache/types'
23import {useLightboxControls} from '#/state/lightbox'
24import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
···40import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
41import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'
42import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'
43+import {IS_IOS} from '#/env'
44import {GrowableAvatar} from './GrowableAvatar'
45import {GrowableBanner} from './GrowableBanner'
46import {StatusBarShadow} from './StatusBarShadow'
···196 }, [profile.banner, moderation, _openLightboxBanner, bannerRef])
197198 return (
199+ <View style={t.atoms.bg} pointerEvents={IS_IOS ? 'auto' : 'box-none'}>
200 <View
201+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
202 style={[a.relative, {height: 150}]}>
203 <StatusBarShadow />
204 <GrowableBanner
···285 a.px_lg,
286 a.pt_xs,
287 a.pb_sm,
288+ IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
289 ]}
290 />
291 ) : (
···295 a.px_lg,
296 a.pt_xs,
297 a.pb_sm,
298+ IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'},
299 ]}
300 />
301 ))}
+2-2
src/screens/Profile/Header/StatusBarShadow.tsx
···5import {useSafeAreaInsets} from 'react-native-safe-area-context'
6import {LinearGradient} from 'expo-linear-gradient'
78-import {isIOS} from '#/platform/detection'
9import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
10import {atoms as a} from '#/alf'
01112const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
13···15 const {top: topInset} = useSafeAreaInsets()
16 const pagerContext = usePagerHeaderContext()
1718- if (isIOS && pagerContext) {
19 const {scrollY} = pagerContext
20 return <StatusBarShadowInnner scrollY={scrollY} />
21 }
···5import {useSafeAreaInsets} from 'react-native-safe-area-context'
6import {LinearGradient} from 'expo-linear-gradient'
708import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
9import {atoms as a} from '#/alf'
10+import {IS_IOS} from '#/env'
1112const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient)
13···15 const {top: topInset} = useSafeAreaInsets()
16 const pagerContext = usePagerHeaderContext()
1718+ if (IS_IOS && pagerContext) {
19 const {scrollY} = pagerContext
20 return <StatusBarShadowInnner scrollY={scrollY} />
21 }
+2-2
src/screens/Profile/Header/SuggestedFollows.tsx
···2import {type AppBskyActorDefs} from '@atproto/api'
34import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
5-import {isAndroid} from '#/platform/detection'
6import {useModerationOpts} from '#/state/preferences/moderation-opts'
7import {
8 useSuggestedFollowsByActorQuery,
···10} from '#/state/queries/suggested-follows'
11import {useBreakpoints} from '#/alf'
12import {ProfileGrid} from '#/components/FeedInterstitials'
01314const DISMISS_ANIMATION_DURATION = 200
15···210 * This issue stems from Android not allowing dragging on clickable elements in the profile header.
211 * Blocking the ability to scroll on Android is too much of a trade-off for now.
212 **/
213- if (isAndroid) return null
214215 return (
216 <AccordionAnimation isExpanded={isExpanded}>
···2import {type AppBskyActorDefs} from '@atproto/api'
34import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation'
05import {useModerationOpts} from '#/state/preferences/moderation-opts'
6import {
7 useSuggestedFollowsByActorQuery,
···9} from '#/state/queries/suggested-follows'
10import {useBreakpoints} from '#/alf'
11import {ProfileGrid} from '#/components/FeedInterstitials'
12+import {IS_ANDROID} from '#/env'
1314const DISMISS_ANIMATION_DURATION = 200
15···210 * This issue stems from Android not allowing dragging on clickable elements in the profile header.
211 * Blocking the ability to scroll on Android is too much of a trade-off for now.
212 **/
213+ if (IS_ANDROID) return null
214215 return (
216 <AccordionAnimation isExpanded={isExpanded}>
+2-2
src/screens/Profile/Header/index.tsx
···17import {useIsFocused} from '@react-navigation/native'
1819import {sanitizeHandle} from '#/lib/strings/handles'
20-import {isNative} from '#/platform/detection'
21import {useProfileShadow} from '#/state/cache/profile-shadow'
22import {useModerationOpts} from '#/state/preferences/moderation-opts'
23import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
···26import {atoms as a, useTheme} from '#/alf'
27import {Header} from '#/components/Layout'
28import * as ProfileCard from '#/components/ProfileCard'
029import {
30 HeaderLabelerButtons,
31 ProfileHeaderLabeler,
···8384 return (
85 <>
86- {isNative && (
87 <MinimalHeader
88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
89 profile={props.profile}
···17import {useIsFocused} from '@react-navigation/native'
1819import {sanitizeHandle} from '#/lib/strings/handles'
020import {useProfileShadow} from '#/state/cache/profile-shadow'
21import {useModerationOpts} from '#/state/preferences/moderation-opts'
22import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
···25import {atoms as a, useTheme} from '#/alf'
26import {Header} from '#/components/Layout'
27import * as ProfileCard from '#/components/ProfileCard'
28+import {IS_NATIVE} from '#/env'
29import {
30 HeaderLabelerButtons,
31 ProfileHeaderLabeler,
···8384 return (
85 <>
86+ {IS_NATIVE && (
87 <MinimalHeader
88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
89 profile={props.profile}
+3-3
src/screens/Profile/ProfileFeed/index.tsx
···17import {type NavigationProp} from '#/lib/routes/types'
18import {makeRecordUri} from '#/lib/strings/url-helpers'
19import {s} from '#/lib/styles'
20-import {isNative} from '#/platform/detection'
21import {listenSoftReset} from '#/state/events'
22import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
23import {
···47} from '#/screens/Profile/components/ProfileFeedHeader'
48import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
49import * as Layout from '#/components/Layout'
05051type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
52export function ProfileFeedScreen(props: Props) {
···175176 const onScrollToTop = useCallback(() => {
177 scrollElRef.current?.scrollToOffset({
178- animated: isNative,
179 offset: 0, // -headerHeight,
180 })
181 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···204 const feedIsVideoMode =
205 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
206 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
207- return isNative && _isVideoFeed
208 }, [feedInfo])
209210 return (
···17import {type NavigationProp} from '#/lib/routes/types'
18import {makeRecordUri} from '#/lib/strings/url-helpers'
19import {s} from '#/lib/styles'
020import {listenSoftReset} from '#/state/events'
21import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
22import {
···46} from '#/screens/Profile/components/ProfileFeedHeader'
47import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
48import * as Layout from '#/components/Layout'
49+import {IS_NATIVE} from '#/env'
5051type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
52export function ProfileFeedScreen(props: Props) {
···175176 const onScrollToTop = useCallback(() => {
177 scrollElRef.current?.scrollToOffset({
178+ animated: IS_NATIVE,
179 offset: 0, // -headerHeight,
180 })
181 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···204 const feedIsVideoMode =
205 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
206 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
207+ return IS_NATIVE && _isVideoFeed
208 }, [feedInfo])
209210 return (
+4-4
src/screens/Profile/Sections/Feed.tsx
···5import {useQueryClient} from '@tanstack/react-query'
67import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
8-import {isIOS, isNative} from '#/platform/detection'
9import {
10 type FeedDescriptor,
11 RQKEY as FEED_RQKEY,
···21import {atoms as a, ios, useTheme} from '#/alf'
22import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
23import {Text} from '#/components/Typography'
024import {type SectionRef} from './types'
2526interface FeedSectionProps {
···53 const [hasNew, setHasNew] = useState(false)
54 const [isScrolledDown, setIsScrolledDown] = useState(false)
55 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
56- const isVideoFeed = isNative && feed.endsWith('posts_with_video')
57 const adjustedInitialNumToRender = useInitialNumToRender({
58 screenHeightOffset: headerHeight,
59 })
60 const onScrollToTop = useCallback(() => {
61 scrollElRef.current?.scrollToOffset({
62- animated: isNative,
63 offset: -headerHeight,
64 })
65 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···85 }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
8687 useEffect(() => {
88- if (isIOS && isFocused && scrollElRef.current) {
89 const nativeTag = findNodeHandle(scrollElRef.current)
90 setScrollViewTag(nativeTag)
91 }
···5import {useQueryClient} from '@tanstack/react-query'
67import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
08import {
9 type FeedDescriptor,
10 RQKEY as FEED_RQKEY,
···20import {atoms as a, ios, useTheme} from '#/alf'
21import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
22import {Text} from '#/components/Typography'
23+import {IS_IOS, IS_NATIVE} from '#/env'
24import {type SectionRef} from './types'
2526interface FeedSectionProps {
···53 const [hasNew, setHasNew] = useState(false)
54 const [isScrolledDown, setIsScrolledDown] = useState(false)
55 const shouldUseAdjustedNumToRender = feed.endsWith('posts_and_author_threads')
56+ const isVideoFeed = IS_NATIVE && feed.endsWith('posts_with_video')
57 const adjustedInitialNumToRender = useInitialNumToRender({
58 screenHeightOffset: headerHeight,
59 })
60 const onScrollToTop = useCallback(() => {
61 scrollElRef.current?.scrollToOffset({
62+ animated: IS_NATIVE,
63 offset: -headerHeight,
64 })
65 truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
···85 }, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
8687 useEffect(() => {
88+ if (IS_IOS && isFocused && scrollElRef.current) {
89 const nativeTag = findNodeHandle(scrollElRef.current)
90 setScrollViewTag(nativeTag)
91 }
+3-3
src/screens/Profile/Sections/Labels.tsx
···10import {useLingui} from '@lingui/react'
1112import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
13-import {isIOS, isNative} from '#/platform/detection'
14import {List, type ListRef} from '#/view/com/util/List'
15import {atoms as a, ios, tokens, useTheme} from '#/alf'
16import {Divider} from '#/components/Divider'
···19import {Loader} from '#/components/Loader'
20import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
21import {Text} from '#/components/Typography'
022import {ErrorState} from '../ErrorState'
23import {type SectionRef} from './types'
24···4950 const onScrollToTop = useCallback(() => {
51 scrollElRef.current?.scrollToOffset({
52- animated: isNative,
53 offset: -headerHeight,
54 })
55 }, [scrollElRef, headerHeight])
···59 }))
6061 useEffect(() => {
62- if (isIOS && isFocused && scrollElRef.current) {
63 const nativeTag = findNodeHandle(scrollElRef.current)
64 setScrollViewTag(nativeTag)
65 }
···10import {useLingui} from '@lingui/react'
1112import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation'
013import {List, type ListRef} from '#/view/com/util/List'
14import {atoms as a, ios, tokens, useTheme} from '#/alf'
15import {Divider} from '#/components/Divider'
···18import {Loader} from '#/components/Loader'
19import {LabelerLabelPreference} from '#/components/moderation/LabelPreference'
20import {Text} from '#/components/Typography'
21+import {IS_IOS, IS_NATIVE} from '#/env'
22import {ErrorState} from '../ErrorState'
23import {type SectionRef} from './types'
24···4950 const onScrollToTop = useCallback(() => {
51 scrollElRef.current?.scrollToOffset({
52+ animated: IS_NATIVE,
53 offset: -headerHeight,
54 })
55 }, [scrollElRef, headerHeight])
···59 }))
6061 useEffect(() => {
62+ if (IS_IOS && isFocused && scrollElRef.current) {
63 const nativeTag = findNodeHandle(scrollElRef.current)
64 setScrollViewTag(nativeTag)
65 }
···3import {Trans} from '@lingui/macro'
45import {logger} from '#/logger'
6-import {isWeb} from '#/platform/detection'
7import {
8 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
9 useTrendingTopics,
···17 TrendingTopicSkeleton,
18} from '#/components/TrendingTopics'
19import {Text} from '#/components/Typography'
02021// Note: This module is not currently used and may be removed in the future.
22···37 <View
38 style={[
39 a.flex_row,
40- isWeb
41 ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
42 : [a.p_lg, a.pt_2xl, a.gap_md],
43 a.border_b,
···3import {Trans} from '@lingui/macro'
45import {logger} from '#/logger'
06import {
7 DEFAULT_LIMIT as RECOMMENDATIONS_COUNT,
8 useTrendingTopics,
···16 TrendingTopicSkeleton,
17} from '#/components/TrendingTopics'
18import {Text} from '#/components/Typography'
19+import {IS_WEB} from '#/env'
2021// Note: This module is not currently used and may be removed in the future.
22···37 <View
38 style={[
39 a.flex_row,
40+ IS_WEB
41 ? [a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
42 : [a.p_lg, a.pt_2xl, a.gap_md],
43 a.border_b,
+4-4
src/screens/Settings/AboutSettings.tsx
···910import {STATUS_PAGE_URL} from '#/lib/constants'
11import {type CommonNavigatorParams} from '#/lib/routes/types'
12-import {isAndroid, isIOS, isNative} from '#/platform/detection'
13import * as Toast from '#/view/com/util/Toast'
14import * as SettingsList from '#/screens/Settings/components/SettingsList'
15import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
···20import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
21import * as Layout from '#/components/Layout'
22import {Loader} from '#/components/Loader'
023import * as env from '#/env'
24import {useDemoMode} from '#/storage/hooks/demo-mode'
25import {useDevMode} from '#/storage/hooks/dev-mode'
···42 return spaceDiff * -1
43 },
44 onSuccess: sizeDiffBytes => {
45- if (isAndroid) {
46 Toast.show(
47 _(
48 msg({
···108 <Trans>System log</Trans>
109 </SettingsList.ItemText>
110 </SettingsList.LinkItem>
111- {isNative && (
112 <SettingsList.PressableItem
113 onPress={() => onClearImageCache()}
114 label={_(msg`Clear image cache`)}
···157 {devModeEnabled && (
158 <>
159 <OTAInfo />
160- {isIOS && (
161 <SettingsList.PressableItem
162 onPress={() => {
163 const newDemoModeEnabled = !demoModeEnabled
···910import {STATUS_PAGE_URL} from '#/lib/constants'
11import {type CommonNavigatorParams} from '#/lib/routes/types'
012import * as Toast from '#/view/com/util/Toast'
13import * as SettingsList from '#/screens/Settings/components/SettingsList'
14import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
···19import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
20import * as Layout from '#/components/Layout'
21import {Loader} from '#/components/Loader'
22+import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env'
23import * as env from '#/env'
24import {useDemoMode} from '#/storage/hooks/demo-mode'
25import {useDevMode} from '#/storage/hooks/dev-mode'
···42 return spaceDiff * -1
43 },
44 onSuccess: sizeDiffBytes => {
45+ if (IS_ANDROID) {
46 Toast.show(
47 _(
48 msg({
···108 <Trans>System log</Trans>
109 </SettingsList.ItemText>
110 </SettingsList.LinkItem>
111+ {IS_NATIVE && (
112 <SettingsList.PressableItem
113 onPress={() => onClearImageCache()}
114 label={_(msg`Clear image cache`)}
···157 {devModeEnabled && (
158 <>
159 <OTAInfo />
160+ {IS_IOS && (
161 <SettingsList.PressableItem
162 onPress={() => {
163 const newDemoModeEnabled = !demoModeEnabled
+2-2
src/screens/Settings/AccessibilitySettings.tsx
···3import {type NativeStackScreenProps} from '@react-navigation/native-stack'
45import {type CommonNavigatorParams} from '#/lib/routes/types'
6-import {isNative} from '#/platform/detection'
7import {
8 useHapticsDisabled,
9 useRequireAltTextEnabled,
···20import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
21import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
22import * as Layout from '#/components/Layout'
02324type Props = NativeStackScreenProps<
25 CommonNavigatorParams,
···76 <Toggle.Platform />
77 </Toggle.Item>
78 </SettingsList.Group>
79- {isNative && (
80 <>
81 <SettingsList.Divider />
82 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
···3import {type NativeStackScreenProps} from '@react-navigation/native-stack'
45import {type CommonNavigatorParams} from '#/lib/routes/types'
06import {
7 useHapticsDisabled,
8 useRequireAltTextEnabled,
···19import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
20import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic'
21import * as Layout from '#/components/Layout'
22+import {IS_NATIVE} from '#/env'
2324type Props = NativeStackScreenProps<
25 CommonNavigatorParams,
···76 <Toggle.Platform />
77 </Toggle.Item>
78 </SettingsList.Group>
79+ {IS_NATIVE && (
80 <>
81 <SettingsList.Divider />
82 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
+3-3
src/screens/Settings/AppIconSettings/index.tsx
···8import {PressableScale} from '#/lib/custom-animations/PressableScale'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
10import {useGate} from '#/lib/statsig/statsig'
11-import {isAndroid} from '#/platform/detection'
12import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
13import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
14import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
···16import * as Toggle from '#/components/forms/Toggle'
17import * as Layout from '#/components/Layout'
18import {Text} from '#/components/Typography'
019import {IS_INTERNAL} from '#/env'
2021type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
···29 )
3031 const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
32- if (isAndroid) {
33 const next =
34 sets.defaults.find(i => i.id === icon) ??
35 sets.core.find(i => i.id === icon)
···221 accessibilityHint={_(msg`Changes app icon`)}
222 targetScale={0.95}
223 onPress={() => {
224- if (isAndroid) {
225 Alert.alert(
226 _(msg`Change app icon to "${icon.name}"`),
227 _(msg`The app will be restarted`),
···8import {PressableScale} from '#/lib/custom-animations/PressableScale'
9import {type CommonNavigatorParams} from '#/lib/routes/types'
10import {useGate} from '#/lib/statsig/statsig'
011import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
12import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
13import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
···15import * as Toggle from '#/components/forms/Toggle'
16import * as Layout from '#/components/Layout'
17import {Text} from '#/components/Typography'
18+import {IS_ANDROID} from '#/env'
19import {IS_INTERNAL} from '#/env'
2021type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
···29 )
3031 const onSetAppIcon = (icon: DynamicAppIcon.IconName) => {
32+ if (IS_ANDROID) {
33 const next =
34 sets.defaults.find(i => i.id === icon) ??
35 sets.core.find(i => i.id === icon)
···221 accessibilityHint={_(msg`Changes app icon`)}
222 targetScale={0.95}
223 onPress={() => {
224+ if (IS_ANDROID) {
225 Alert.alert(
226 _(msg`Change app icon to "${icon.name}"`),
227 _(msg`The app will be restarted`),
+52-2
src/screens/Settings/AppearanceSettings.tsx
···13 type CommonNavigatorParams,
14 type NativeStackScreenProps,
15} from '#/lib/routes/types'
16-import {isNative} from '#/platform/detection'
17import {
18 useEnableSquareAvatars,
19 useSetEnableSquareAvatars,
···39import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
40import * as Layout from '#/components/Layout'
41import {Text} from '#/components/Typography'
042import {IS_INTERNAL} from '#/env'
43import * as SettingsList from './components/SettingsList'
44···308 </Toggle.Item>
309 </SettingsList.Group>
310311- {isNative && IS_INTERNAL && (
00000000000000000000000000000000000000000000000000312 <>
313 <SettingsList.Divider />
314 <AppIconSettingsListItem />
···13import {useLingui} from '@lingui/react'
14import {useMutation} from '@tanstack/react-query'
1516-import {isWeb} from '#/platform/detection'
17import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
18import {atoms as a, native, useTheme} from '#/alf'
19import {Admonition} from '#/components/Admonition'
···24import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
25import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
26import {Text} from '#/components/Typography'
027import {CopyButton} from './CopyButton'
2829export function AddAppPasswordDialog({
···181 ) : (
182 <Animated.View
183 style={[a.gap_lg]}
184- entering={isWeb ? FadeIn.delay(200) : SlideInRight}
185 key={1}>
186 <Text style={[a.text_2xl, a.font_semi_bold]}>
187 <Trans>Here is your app password!</Trans>
···13import {useLingui} from '@lingui/react'
14import {useMutation} from '@tanstack/react-query'
15016import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
17import {atoms as a, native, useTheme} from '#/alf'
18import {Admonition} from '#/components/Admonition'
···23import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
24import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
25import {Text} from '#/components/Typography'
26+import {IS_WEB} from '#/env'
27import {CopyButton} from './CopyButton'
2829export function AddAppPasswordDialog({
···181 ) : (
182 <Animated.View
183 style={[a.gap_lg]}
184+ entering={IS_WEB ? FadeIn.delay(200) : SlideInRight}
185 key={1}>
186 <Text style={[a.text_2xl, a.font_semi_bold]}>
187 <Trans>Here is your app password!</Trans>
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
7-import {isNative} from '#/platform/detection'
8import {useAgent, useSession} from '#/state/session'
9import {pdsAgent} from '#/state/session/agent'
10import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
···16import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
17import {Loader} from '#/components/Loader'
18import {P, Text} from '#/components/Typography'
01920enum Stages {
21 Email,
···194 </View>
195 ) : undefined}
196197- {!gtMobile && isNative && <View style={{height: 40}} />}
198 </View>
199 </Dialog.ScrollableInner>
200 </Dialog.Outer>
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
07import {useAgent, useSession} from '#/state/session'
8import {pdsAgent} from '#/state/session/agent'
9import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
···15import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
16import {Loader} from '#/components/Loader'
17import {P, Text} from '#/components/Typography'
18+import {IS_NATIVE} from '#/env'
1920enum Stages {
21 Email,
···194 </View>
195 ) : undefined}
196197+ {!gtMobile && IS_NATIVE && <View style={{height: 40}} />}
198 </View>
199 </Dialog.ScrollableInner>
200 </Dialog.Outer>
+8-6
src/screens/Signup/StepCaptcha/index.tsx
···78import {createFullHandle} from '#/lib/strings/handles'
9import {logger} from '#/logger'
10-import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
11import {useSignupContext} from '#/screens/Signup/state'
12import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
13import {atoms as a, useTheme} from '#/alf'
14import {FormError} from '#/components/forms/FormError'
015import {GCP_PROJECT_ID} from '#/env'
16import {BackNextButtons} from '../BackNextButtons'
1718const CAPTCHA_PATH =
19- isWeb || GCP_PROJECT_ID === 0 ? '/gate/signup' : '/gate/signup/attempt-attest'
002021export function StepCaptcha() {
22- if (isWeb) {
23 return <StepCaptchaInner />
24 } else {
25 return <StepCaptchaNative />
···35 ;(async () => {
36 logger.debug('trying to generate attestation token...')
37 try {
38- if (isIOS) {
39 logger.debug('starting to generate devicecheck token...')
40 const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
41 setToken(token)
···86 newUrl.searchParams.set('state', stateParam)
87 newUrl.searchParams.set('colorScheme', theme.name)
8889- if (isNative && token) {
90 newUrl.searchParams.set('platform', Platform.OS)
91 newUrl.searchParams.set('token', token)
92- if (isAndroid && payload) {
93 newUrl.searchParams.set('payload', payload)
94 }
95 }
···78import {createFullHandle} from '#/lib/strings/handles'
9import {logger} from '#/logger'
010import {useSignupContext} from '#/screens/Signup/state'
11import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
12import {atoms as a, useTheme} from '#/alf'
13import {FormError} from '#/components/forms/FormError'
14+import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
15import {GCP_PROJECT_ID} from '#/env'
16import {BackNextButtons} from '../BackNextButtons'
1718const CAPTCHA_PATH =
19+ IS_WEB || GCP_PROJECT_ID === 0
20+ ? '/gate/signup'
21+ : '/gate/signup/attempt-attest'
2223export function StepCaptcha() {
24+ if (IS_WEB) {
25 return <StepCaptchaInner />
26 } else {
27 return <StepCaptchaNative />
···37 ;(async () => {
38 logger.debug('trying to generate attestation token...')
39 try {
40+ if (IS_IOS) {
41 logger.debug('starting to generate devicecheck token...')
42 const token = await ReactNativeDeviceAttest.getDeviceCheckToken()
43 setToken(token)
···88 newUrl.searchParams.set('state', stateParam)
89 newUrl.searchParams.set('colorScheme', theme.name)
9091+ if (IS_NATIVE && token) {
92 newUrl.searchParams.set('platform', Platform.OS)
93 newUrl.searchParams.set('token', token)
94+ if (IS_ANDROID && payload) {
95 newUrl.searchParams.set('payload', payload)
96 }
97 }
+4-4
src/screens/Signup/StepInfo/index.tsx
···8import {DEFAULT_SERVICE} from '#/lib/constants'
9import {isEmailMaybeInvalid} from '#/lib/strings/email'
10import {logger} from '#/logger'
11-import {isNative, isWeb} from '#/platform/detection'
12import {useSignupContext} from '#/screens/Signup/state'
13import {Policies} from '#/screens/Signup/StepInfo/Policies'
14import {atoms as a, native} from '#/alf'
···37 MIN_ACCESS_AGE,
38 useAgeAssuranceRegionConfigWithFallback,
39} from '#/ageAssurance/util'
040import {
41 useDeviceGeolocationApi,
42 useIsDeviceGeolocationGranted,
···215 If you have one, sign in with an existing Bluesky account.
216 </Trans>
217 </Text>
218- <View style={isWeb && [a.flex_row, a.justify_center]}>
219 <Button
220 testID="signInButton"
221 onPress={onPressSignIn}
···397 </Trans>
398 )}
399 </Admonition.Text>
400- {isNative &&
401 !isDeviceGeolocationGranted &&
402 isOverAppMinAccessAge && (
403 <Admonition.Text>
···429 ) : undefined}
430 </View>
431432- {isNative && (
433 <DeviceLocationRequestDialog
434 control={locationControl}
435 onLocationAcquired={props => {
···8import {DEFAULT_SERVICE} from '#/lib/constants'
9import {isEmailMaybeInvalid} from '#/lib/strings/email'
10import {logger} from '#/logger'
011import {useSignupContext} from '#/screens/Signup/state'
12import {Policies} from '#/screens/Signup/StepInfo/Policies'
13import {atoms as a, native} from '#/alf'
···36 MIN_ACCESS_AGE,
37 useAgeAssuranceRegionConfigWithFallback,
38} from '#/ageAssurance/util'
39+import {IS_NATIVE, IS_WEB} from '#/env'
40import {
41 useDeviceGeolocationApi,
42 useIsDeviceGeolocationGranted,
···215 If you have one, sign in with an existing Bluesky account.
216 </Trans>
217 </Text>
218+ <View style={IS_WEB && [a.flex_row, a.justify_center]}>
219 <Button
220 testID="signInButton"
221 onPress={onPressSignIn}
···397 </Trans>
398 )}
399 </Admonition.Text>
400+ {IS_NATIVE &&
401 !isDeviceGeolocationGranted &&
402 isOverAppMinAccessAge && (
403 <Admonition.Text>
···429 ) : undefined}
430 </View>
431432+ {IS_NATIVE && (
433 <DeviceLocationRequestDialog
434 control={locationControl}
435 onLocationAcquired={props => {
+2-2
src/screens/Signup/index.tsx
···89import {FEEDBACK_FORM_URL} from '#/lib/constants'
10import {logger} from '#/logger'
11-import {isAndroid} from '#/platform/detection'
12import {useServiceQuery} from '#/state/queries/service'
13import {useStarterPackQuery} from '#/state/queries/starter-packs'
14import {useActiveStarterPack} from '#/state/shell/starter-pack'
···30import {InlineLinkText} from '#/components/Link'
31import {ScreenTransition} from '#/components/ScreenTransition'
32import {Text} from '#/components/Typography'
033import {GCP_PROJECT_ID} from '#/env'
34import * as bsky from '#/types/bsky'
35···114115 // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen.
116 useEffect(() => {
117- if (!isAndroid) {
118 return
119 }
120 ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err =>
···89import {FEEDBACK_FORM_URL} from '#/lib/constants'
10import {logger} from '#/logger'
011import {useServiceQuery} from '#/state/queries/service'
12import {useStarterPackQuery} from '#/state/queries/starter-packs'
13import {useActiveStarterPack} from '#/state/shell/starter-pack'
···29import {InlineLinkText} from '#/components/Link'
30import {ScreenTransition} from '#/components/ScreenTransition'
31import {Text} from '#/components/Typography'
32+import {IS_ANDROID} from '#/env'
33import {GCP_PROJECT_ID} from '#/env'
34import * as bsky from '#/types/bsky'
35···114115 // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen.
116 useEffect(() => {
117+ if (!IS_ANDROID) {
118 return
119 }
120 ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err =>
+3-3
src/screens/SignupQueued.tsx
···6import {useLingui} from '@lingui/react'
78import {logger} from '#/logger'
9-import {isIOS, isWeb} from '#/platform/detection'
10import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
11import {pdsAgent} from '#/state/session/agent'
12import {useOnboardingDispatch} from '#/state/shell'
···15import {Button, ButtonIcon, ButtonText} from '#/components/Button'
16import {Loader} from '#/components/Loader'
17import {P, Text} from '#/components/Typography'
01819const COL_WIDTH = 400
20···99 </Button>
100 )
101102- const webLayout = isWeb && gtMobile
103104 return (
105 <Modal
···107 animationType={native('slide')}
108 presentationStyle="formSheet"
109 style={[web(a.util_screen_outer)]}>
110- {isIOS && <SystemBars style={{statusBar: 'light'}} />}
111 <ScrollView
112 style={[a.flex_1, t.atoms.bg]}
113 contentContainerStyle={{borderWidth: 0}}
···6import {useLingui} from '@lingui/react'
78import {logger} from '#/logger'
09import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
10import {pdsAgent} from '#/state/session/agent'
11import {useOnboardingDispatch} from '#/state/shell'
···14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import {Loader} from '#/components/Loader'
16import {P, Text} from '#/components/Typography'
17+import {IS_IOS, IS_WEB} from '#/env'
1819const COL_WIDTH = 400
20···99 </Button>
100 )
101102+ const webLayout = IS_WEB && gtMobile
103104 return (
105 <Modal
···107 animationType={native('slide')}
108 presentationStyle="formSheet"
109 style={[web(a.util_screen_outer)]}>
110+ {IS_IOS && <SystemBars style={{statusBar: 'light'}} />}
111 <ScrollView
112 style={[a.flex_1, t.atoms.bg]}
113 contentContainerStyle={{borderWidth: 0}}
···22let _state: Schema = defaults
23const _emitter = new EventEmitter()
2425+// async, to match native implementation
26+// eslint-disable-next-line @typescript-eslint/require-await
27export async function init() {
28 broadcast.onmessage = onBroadcastMessage
29 window.onstorage = onStorage
···39}
40get satisfies PersistedApi['get']
4142+// eslint-disable-next-line @typescript-eslint/require-await
43export async function write<K extends keyof Schema>(
44 key: K,
45 value: Schema[K],
···85}
86onUpdate satisfies PersistedApi['onUpdate']
8788+// eslint-disable-next-line @typescript-eslint/require-await
89export async function clearStorage() {
90 try {
91 localStorage.removeItem(BSKY_STORAGE)
···106 }
107}
108109+// eslint-disable-next-line @typescript-eslint/require-await
110async function onBroadcastMessage({data}: MessageEvent) {
111 if (
112 typeof data === 'object' &&
+1-1
src/state/preferences/index.tsx
···51 useSetExternalEmbedPref,
52} from './external-embeds-prefs'
53export {useGoLinksEnabled, useSetGoLinksEnabled} from './go-links-enabled'
54-export * from './hidden-posts'
55export {
56 useHideFeedsPromoTab,
57 useSetHideFeedsPromoTab,
···51 useSetExternalEmbedPref,
52} from './external-embeds-prefs'
53export {useGoLinksEnabled, useSetGoLinksEnabled} from './go-links-enabled'
54+export {useHiddenPosts, useHiddenPostsApi} from './hidden-posts'
55export {
56 useHideFeedsPromoTab,
57 useSetHideFeedsPromoTab,
+2-2
src/state/preferences/kawaii.tsx
···1import React from 'react'
23-import {isWeb} from '#/platform/detection'
4import * as persisted from '#/state/persisted'
056type StateContext = persisted.Schema['kawaii']
7···30 React.useEffect(() => {
31 // dumb and stupid but it's web only so just refresh the page if you want to change it
3233- if (isWeb) {
34 const kawaii = new URLSearchParams(window.location.search).get('kawaii')
35 switch (kawaii) {
36 case 'true':
···1import React from 'react'
203import * as persisted from '#/state/persisted'
4+import {IS_WEB} from '#/env'
56type StateContext = persisted.Schema['kawaii']
7···30 React.useEffect(() => {
31 // dumb and stupid but it's web only so just refresh the page if you want to change it
3233+ if (IS_WEB) {
34 const kawaii = new URLSearchParams(window.location.search).get('kawaii')
35 switch (kawaii) {
36 case 'true':
···1import React, {useMemo} from 'react'
2import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
34-import {isWeb} from '#/platform/detection'
5import * as persisted from '#/state/persisted'
6import {useCloseAllActiveElements} from '#/state/util'
7import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
08import {emitSessionDropped} from '../events'
9import {
10 agentToSessionAccount,
···341 )
342343 // @ts-expect-error window type is not declared, debug only
344- if (__DEV__ && isWeb) window.agent = state.currentAgentState.agent
345346 const agent = state.currentAgentState.agent as BskyAppAgent
347 const currentAgentRef = React.useRef(agent)
···1import React, {useMemo} from 'react'
2import {type AtpSessionEvent, type BskyAgent} from '@atproto/api'
304import * as persisted from '#/state/persisted'
5import {useCloseAllActiveElements} from '#/state/util'
6import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
7+import {IS_WEB} from '#/env'
8import {emitSessionDropped} from '../events'
9import {
10 agentToSessionAccount,
···341 )
342343 // @ts-expect-error window type is not declared, debug only
344+ if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent
345346 const agent = state.currentAgentState.agent as BskyAppAgent
347 const currentAgentRef = React.useRef(agent)
+3-3
src/state/shell/logged-out.tsx
···1import React from 'react'
23-import {isWeb} from '#/platform/detection'
4import {useSession} from '#/state/session'
5import {useActiveStarterPack} from '#/state/shell/starter-pack'
067type State = {
8 showLoggedOut: boolean
···26 /**
27 * The did of the account to populate the login form with.
28 */
29- requestedAccount?: string | 'none' | 'new' | 'starterpack'
30 }) => void
31 /**
32 * Clears the requested account so that next time the logged out view is
···55 const [state, setState] = React.useState<State>({
56 showLoggedOut: shouldShowStarterPack,
57 requestedAccountSwitchTo: shouldShowStarterPack
58- ? isWeb
59 ? 'starterpack'
60 : 'new'
61 : undefined,
···1import React from 'react'
203import {useSession} from '#/state/session'
4import {useActiveStarterPack} from '#/state/shell/starter-pack'
5+import {IS_WEB} from '#/env'
67type State = {
8 showLoggedOut: boolean
···26 /**
27 * The did of the account to populate the login form with.
28 */
29+ requestedAccount?: (string & {}) | 'none' | 'new' | 'starterpack'
30 }) => void
31 /**
32 * Clears the requested account so that next time the logged out view is
···55 const [state, setState] = React.useState<State>({
56 showLoggedOut: shouldShowStarterPack,
57 requestedAccountSwitchTo: shouldShowStarterPack
58+ ? IS_WEB
59 ? 'starterpack'
60 : 'new'
61 : undefined,
+3-3
src/state/shell/selected-feed.tsx
···1import {createContext, useCallback, useContext, useState} from 'react'
23-import {isWeb} from '#/platform/detection'
4import {type FeedDescriptor} from '#/state/queries/post-feed'
5import {useSession} from '#/state/session'
06import {account} from '#/storage'
78type StateContext = FeedDescriptor | null
···14setContext.displayName = 'SelectedFeedSetContext'
1516function getInitialFeed(did?: string): FeedDescriptor | null {
17- if (isWeb) {
18 if (window.location.pathname === '/') {
19 const params = new URLSearchParams(window.location.search)
20 const feedFromUrl = params.get('feed')
···49 const saveState = useCallback(
50 (feed: FeedDescriptor) => {
51 setState(feed)
52- if (isWeb) {
53 try {
54 sessionStorage.setItem('lastSelectedHomeFeed', feed)
55 } catch {}
···1import {createContext, useCallback, useContext, useState} from 'react'
203import {type FeedDescriptor} from '#/state/queries/post-feed'
4import {useSession} from '#/state/session'
5+import {IS_WEB} from '#/env'
6import {account} from '#/storage'
78type StateContext = FeedDescriptor | null
···14setContext.displayName = 'SelectedFeedSetContext'
1516function getInitialFeed(did?: string): FeedDescriptor | null {
17+ if (IS_WEB) {
18 if (window.location.pathname === '/') {
19 const params = new URLSearchParams(window.location.search)
20 const feedFromUrl = params.get('feed')
···49 const saveState = useCallback(
50 (feed: FeedDescriptor) => {
51 setState(feed)
52+ if (IS_WEB) {
53 try {
54 sessionStorage.setItem('lastSelectedHomeFeed', feed)
55 } catch {}
···76import {cleanError} from '#/lib/strings/errors'
77import {colors} from '#/lib/styles'
78import {logger} from '#/logger'
79-import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
80import {useDialogStateControlContext} from '#/state/dialogs'
81import {emitPostCreated} from '#/state/events'
82import {
···132import * as Prompt from '#/components/Prompt'
133import * as Toast from '#/components/Toast'
134import {Text as NewText} from '#/components/Typography'
0135import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
136import {PostLanguageSelect} from './select-language/PostLanguageSelect'
137import {
···331 const insets = useSafeAreaInsets()
332 const viewStyles = useMemo(
333 () => ({
334- paddingTop: isAndroid ? insets.top : 0,
335 paddingBottom:
336 // iOS - when keyboard is closed, keep the bottom bar in the safe area
337- (isIOS && !isKeyboardVisible) ||
338 // Android - Android >=35 KeyboardAvoidingView adds double padding when
339 // keyboard is closed, so we subtract that in the offset and add it back
340 // here when the keyboard is open
341- (isAndroid && isKeyboardVisible)
342 ? insets.bottom
343 : 0,
344 }),
···368369 // On Android, pressing Back should ask confirmation.
370 useEffect(() => {
371- if (!isAndroid) {
372 return
373 }
374 const backHandler = BackHandler.addEventListener(
···673 composerState.mutableNeedsFocusActive = false
674 // On Android, this risks getting the cursor stuck behind the keyboard.
675 // Not worth it.
676- if (!isAndroid) {
677 textInput.current?.focus()
678 }
679 }
···729 </>
730 )
731732- const isWebFooterSticky = !isNative && thread.posts.length > 1
733 return (
734 <BottomSheetPortalProvider>
735 <KeyboardAvoidingView
736 testID="composePostView"
737- behavior={isIOS ? 'padding' : 'height'}
738 keyboardVerticalOffset={keyboardVerticalOffset}
739 style={a.flex_1}>
740 <View
···794 onPublish={onComposerPostPublish}
795 onError={setError}
796 />
797- {isWebFooterSticky && post.id === activePost.id && (
798 <View style={styles.stickyFooterWeb}>{footer}</View>
799 )}
800 </React.Fragment>
801 ))}
802 </Animated.ScrollView>
803- {!isWebFooterSticky && footer}
804 </View>
805806 <Prompt.Basic
···853 const {data: currentProfile} = useProfileQuery({did: currentDid})
854 const richtext = post.richtext
855 const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
856- const forceMinHeight = isWeb && isTextOnly && isActive
857 const selectTextInputPlaceholder = isReply
858 ? isFirstPost
859 ? _(msg`Write your reply`)
···895 async (uri: string) => {
896 if (
897 uri.startsWith('data:video/') ||
898- (isWeb && uri.startsWith('data:image/gif'))
899 ) {
900- if (isNative) return // web only
901 const [mimeType] = uri.slice('data:'.length).split(';')
902 if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
903 Toast.show(_(msg`Unsupported video type: ${mimeType}`), {
···927 a.mb_sm,
928 !isActive && isLastPost && a.mb_lg,
929 !isActive && styles.inactivePost,
930- isTextOnly && isNative && a.flex_grow,
931 ]}>
932- <View style={[a.flex_row, isNative && a.flex_1]}>
933 <UserAvatar
934 avatar={currentProfile?.avatar}
935 size={42}
···1270 </LayoutAnimationConfig>
1271 {embed.quote?.uri ? (
1272 <View
1273- style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}>
1274 <View style={[a.relative]}>
1275 <View style={{pointerEvents: 'none'}}>
1276 <LazyQuoteEmbed uri={embed.quote.uri} />
···1683 const {top, bottom} = useSafeAreaInsets()
16841685 // Android etc
1686- if (!isIOS) {
1687 // need to account for the edge-to-edge nav bar
1688 return bottom * -1
1689 }
···1727 const appState = useAppState()
17281729 useEffect(() => {
1730- if (isIOS) {
1731 if (appState === 'inactive') {
1732 Keyboard.dismiss()
1733 }
···1879 style: StyleProp<ViewStyle>
1880 children: React.ReactNode
1881}) {
1882- if (isWeb) return children
1883 return (
1884 <Animated.View
1885 style={style}
···76import {cleanError} from '#/lib/strings/errors'
77import {colors} from '#/lib/styles'
78import {logger} from '#/logger'
079import {useDialogStateControlContext} from '#/state/dialogs'
80import {emitPostCreated} from '#/state/events'
81import {
···131import * as Prompt from '#/components/Prompt'
132import * as Toast from '#/components/Toast'
133import {Text as NewText} from '#/components/Typography'
134+import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
135import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
136import {PostLanguageSelect} from './select-language/PostLanguageSelect'
137import {
···331 const insets = useSafeAreaInsets()
332 const viewStyles = useMemo(
333 () => ({
334+ paddingTop: IS_ANDROID ? insets.top : 0,
335 paddingBottom:
336 // iOS - when keyboard is closed, keep the bottom bar in the safe area
337+ (IS_IOS && !isKeyboardVisible) ||
338 // Android - Android >=35 KeyboardAvoidingView adds double padding when
339 // keyboard is closed, so we subtract that in the offset and add it back
340 // here when the keyboard is open
341+ (IS_ANDROID && isKeyboardVisible)
342 ? insets.bottom
343 : 0,
344 }),
···368369 // On Android, pressing Back should ask confirmation.
370 useEffect(() => {
371+ if (!IS_ANDROID) {
372 return
373 }
374 const backHandler = BackHandler.addEventListener(
···673 composerState.mutableNeedsFocusActive = false
674 // On Android, this risks getting the cursor stuck behind the keyboard.
675 // Not worth it.
676+ if (!IS_ANDROID) {
677 textInput.current?.focus()
678 }
679 }
···729 </>
730 )
731732+ const IS_WEBFooterSticky = !IS_NATIVE && thread.posts.length > 1
733 return (
734 <BottomSheetPortalProvider>
735 <KeyboardAvoidingView
736 testID="composePostView"
737+ behavior={IS_IOS ? 'padding' : 'height'}
738 keyboardVerticalOffset={keyboardVerticalOffset}
739 style={a.flex_1}>
740 <View
···794 onPublish={onComposerPostPublish}
795 onError={setError}
796 />
797+ {IS_WEBFooterSticky && post.id === activePost.id && (
798 <View style={styles.stickyFooterWeb}>{footer}</View>
799 )}
800 </React.Fragment>
801 ))}
802 </Animated.ScrollView>
803+ {!IS_WEBFooterSticky && footer}
804 </View>
805806 <Prompt.Basic
···853 const {data: currentProfile} = useProfileQuery({did: currentDid})
854 const richtext = post.richtext
855 const isTextOnly = !post.embed.link && !post.embed.quote && !post.embed.media
856+ const forceMinHeight = IS_WEB && isTextOnly && isActive
857 const selectTextInputPlaceholder = isReply
858 ? isFirstPost
859 ? _(msg`Write your reply`)
···895 async (uri: string) => {
896 if (
897 uri.startsWith('data:video/') ||
898+ (IS_WEB && uri.startsWith('data:image/gif'))
899 ) {
900+ if (IS_NATIVE) return // web only
901 const [mimeType] = uri.slice('data:'.length).split(';')
902 if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
903 Toast.show(_(msg`Unsupported video type: ${mimeType}`), {
···927 a.mb_sm,
928 !isActive && isLastPost && a.mb_lg,
929 !isActive && styles.inactivePost,
930+ isTextOnly && IS_NATIVE && a.flex_grow,
931 ]}>
932+ <View style={[a.flex_row, IS_NATIVE && a.flex_1]}>
933 <UserAvatar
934 avatar={currentProfile?.avatar}
935 size={42}
···1270 </LayoutAnimationConfig>
1271 {embed.quote?.uri ? (
1272 <View
1273+ style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], IS_WEB && [a.pb_md]]}>
1274 <View style={[a.relative]}>
1275 <View style={{pointerEvents: 'none'}}>
1276 <LazyQuoteEmbed uri={embed.quote.uri} />
···1683 const {top, bottom} = useSafeAreaInsets()
16841685 // Android etc
1686+ if (!IS_IOS) {
1687 // need to account for the edge-to-edge nav bar
1688 return bottom * -1
1689 }
···1727 const appState = useAppState()
17281729 useEffect(() => {
1730+ if (IS_IOS) {
1731 if (appState === 'inactive') {
1732 Keyboard.dismiss()
1733 }
···1879 style: StyleProp<ViewStyle>
1880 children: React.ReactNode
1881}) {
1882+ if (IS_WEB) return children
1883 return (
1884 <Animated.View
1885 style={style}
+2-2
src/view/com/composer/GifAltText.tsx
···9 type EmbedPlayerParams,
10 parseEmbedPlayerFromUrl,
11} from '#/lib/strings/embed-player'
12-import {isAndroid} from '#/platform/detection'
13import {useResolveGifQuery} from '#/state/queries/resolve-link'
14import {type Gif} from '#/state/queries/tenor'
15import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
···23import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
24import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
25import {Text} from '#/components/Typography'
026import {AltTextReminder} from './photos/Gallery'
2728export function GifAltTextDialog({
···224 </View>
225 <Dialog.Close />
226 {/* Maybe fix this later -h */}
227- {isAndroid ? <View style={{height: 300}} /> : null}
228 </Dialog.ScrollableInner>
229 )
230}
···9 type EmbedPlayerParams,
10 parseEmbedPlayerFromUrl,
11} from '#/lib/strings/embed-player'
012import {useResolveGifQuery} from '#/state/queries/resolve-link'
13import {type Gif} from '#/state/queries/tenor'
14import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
···22import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
23import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
24import {Text} from '#/components/Typography'
25+import {IS_ANDROID} from '#/env'
26import {AltTextReminder} from './photos/Gallery'
2728export function GifAltTextDialog({
···224 </View>
225 <Dialog.Close />
226 {/* Maybe fix this later -h */}
227+ {IS_ANDROID ? <View style={{height: 300}} /> : null}
228 </Dialog.ScrollableInner>
229 )
230}
+2-2
src/view/com/composer/KeyboardAccessory.tsx
···3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import type React from 'react'
56-import {isWeb} from '#/platform/detection'
7import {atoms as a, useTheme} from '#/alf'
089export function KeyboardAccessory({children}: {children: React.ReactNode}) {
10 const t = useTheme()
···22 ]
2324 // todo: when iPad support is added, it should also not use the KeyboardStickyView
25- if (isWeb) {
26 return <View style={style}>{children}</View>
27 }
28
···3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import type React from 'react'
506import {atoms as a, useTheme} from '#/alf'
7+import {IS_WEB} from '#/env'
89export function KeyboardAccessory({children}: {children: React.ReactNode}) {
10 const t = useTheme()
···22 ]
2324 // todo: when iPad support is added, it should also not use the KeyboardStickyView
25+ if (IS_WEB) {
26 return <View style={style}>{children}</View>
27 }
28
+9-9
src/view/com/composer/SelectMediaButton.tsx
···11} from '#/lib/hooks/usePermissions'
12import {openUnifiedPicker} from '#/lib/media/picker'
13import {extractDataUriMime} from '#/lib/media/util'
14-import {isNative, isWeb} from '#/platform/detection'
15import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
16import {MAX_IMAGES} from '#/view/com/composer/state/composer'
17import {atoms as a, useTheme} from '#/alf'
···19import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
20import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
21import * as toast from '#/components/Toast'
02223export type SelectMediaButtonProps = {
24 disabled?: boolean
···92 'image/svg+xml',
93 'image/webp',
94 'image/avif',
95- isNative && 'image/heic',
96 ] as const
97).filter(Boolean)
98type SupportedImageMimeType = Exclude<
···262 * We don't care too much about mimeType at this point on native,
263 * since the `processVideo` step later on will convert to `.mp4`.
264 */
265- if (isWeb && !isSupportedVideoMimeType(mimeType)) {
266 errors.add(SelectedAssetError.Unsupported)
267 continue
268 }
···272 * to filter out large files on web. On native, we compress these anyway,
273 * so we only check on web.
274 */
275- if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
276 errors.add(SelectedAssetError.FileTooBig)
277 continue
278 }
···291 * to filter out large files on web. On native, we compress GIFs as
292 * videos anyway, so we only check on web.
293 */
294- if (isWeb && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
295 errors.add(SelectedAssetError.FileTooBig)
296 continue
297 }
···309 * base64 data-uri, so we construct it here for web only.
310 */
311 uri:
312- isWeb && asset.base64
313 ? `data:${mimeType};base64,${asset.base64}`
314 : asset.uri,
315 })
···328 }
329330 if (supportedAssets[0].duration) {
331- if (isWeb) {
332 /*
333 * Web reports duration as seconds
334 */
···433 )
434435 const onPressSelectMedia = useCallback(async () => {
436- if (isNative) {
437 const [photoAccess, videoAccess] = await Promise.all([
438 requestPhotoAccessIfNeeded(),
439 requestVideoAccessIfNeeded(),
···447 }
448 }
449450- if (isNative && Keyboard.isVisible()) {
451 Keyboard.dismiss()
452 }
453
···11} from '#/lib/hooks/usePermissions'
12import {openUnifiedPicker} from '#/lib/media/picker'
13import {extractDataUriMime} from '#/lib/media/util'
014import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
15import {MAX_IMAGES} from '#/view/com/composer/state/composer'
16import {atoms as a, useTheme} from '#/alf'
···18import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
19import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
20import * as toast from '#/components/Toast'
21+import {IS_NATIVE, IS_WEB} from '#/env'
2223export type SelectMediaButtonProps = {
24 disabled?: boolean
···92 'image/svg+xml',
93 'image/webp',
94 'image/avif',
95+ IS_NATIVE && 'image/heic',
96 ] as const
97).filter(Boolean)
98type SupportedImageMimeType = Exclude<
···262 * We don't care too much about mimeType at this point on native,
263 * since the `processVideo` step later on will convert to `.mp4`.
264 */
265+ if (IS_WEB && !isSupportedVideoMimeType(mimeType)) {
266 errors.add(SelectedAssetError.Unsupported)
267 continue
268 }
···272 * to filter out large files on web. On native, we compress these anyway,
273 * so we only check on web.
274 */
275+ if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
276 errors.add(SelectedAssetError.FileTooBig)
277 continue
278 }
···291 * to filter out large files on web. On native, we compress GIFs as
292 * videos anyway, so we only check on web.
293 */
294+ if (IS_WEB && asset.fileSize && asset.fileSize > VIDEO_MAX_SIZE) {
295 errors.add(SelectedAssetError.FileTooBig)
296 continue
297 }
···309 * base64 data-uri, so we construct it here for web only.
310 */
311 uri:
312+ IS_WEB && asset.base64
313 ? `data:${mimeType};base64,${asset.base64}`
314 : asset.uri,
315 })
···328 }
329330 if (supportedAssets[0].duration) {
331+ if (IS_WEB) {
332 /*
333 * Web reports duration as seconds
334 */
···433 )
434435 const onPressSelectMedia = useCallback(async () => {
436+ if (IS_NATIVE) {
437 const [photoAccess, videoAccess] = await Promise.all([
438 requestPhotoAccessIfNeeded(),
439 requestVideoAccessIfNeeded(),
···447 }
448 }
449450+ if (IS_NATIVE && Keyboard.isVisible()) {
451 Keyboard.dismiss()
452 }
453
+2-2
src/view/com/composer/labels/LabelsBtn.tsx
···9 type OtherSelfLabel,
10 type SelfLabel,
11} from '#/lib/moderation'
12-import {isWeb} from '#/platform/detection'
13import {atoms as a, useTheme, web} from '#/alf'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import * as Dialog from '#/components/Dialog'
···18import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
19import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
20import {Text} from '#/components/Typography'
02122export function LabelsBtn({
23 labels,
···218 label={_(msg`Done`)}
219 onPress={() => control.close()}
220 color="primary"
221- size={isWeb ? 'small' : 'large'}
222 variant="solid"
223 testID="confirmBtn">
224 <ButtonText>
···9 type OtherSelfLabel,
10 type SelfLabel,
11} from '#/lib/moderation'
012import {atoms as a, useTheme, web} from '#/alf'
13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
···17import {TinyChevronBottom_Stroke2_Corner0_Rounded as TinyChevronIcon} from '#/components/icons/Chevron'
18import {Shield_Stroke2_Corner0_Rounded} from '#/components/icons/Shield'
19import {Text} from '#/components/Typography'
20+import {IS_WEB} from '#/env'
2122export function LabelsBtn({
23 labels,
···218 label={_(msg`Done`)}
219 onPress={() => control.close()}
220 color="primary"
221+ size={IS_WEB ? 'small' : 'large'}
222 variant="solid"
223 testID="confirmBtn">
224 <ButtonText>
+2-2
src/view/com/composer/photos/Gallery.tsx
···16import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
17import {type Dimensions} from '#/lib/media/types'
18import {colors, s} from '#/lib/styles'
19-import {isNative} from '#/platform/detection'
20import {type ComposerImage, cropImage} from '#/state/gallery'
21import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
22import {Text} from '#/view/com/util/text/Text'
23import {tokens, useTheme} from '#/alf'
24import * as Dialog from '#/components/Dialog'
25import {MediaInsetBorder} from '#/components/MediaInsetBorder'
026import {type PostAction} from '../state/composer'
27import {EditImageDialog} from './EditImageDialog'
28import {ImageAltTextDialog} from './ImageAltTextDialog'
···148 const enableSquareButtons = useEnableSquareButtons()
149150 const onImageEdit = () => {
151- if (isNative) {
152 cropImage(image).then(next => {
153 onChange(next)
154 })
···16import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
17import {type Dimensions} from '#/lib/media/types'
18import {colors, s} from '#/lib/styles'
019import {type ComposerImage, cropImage} from '#/state/gallery'
20import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
21import {Text} from '#/view/com/util/text/Text'
22import {tokens, useTheme} from '#/alf'
23import * as Dialog from '#/components/Dialog'
24import {MediaInsetBorder} from '#/components/MediaInsetBorder'
25+import {IS_NATIVE} from '#/env'
26import {type PostAction} from '../state/composer'
27import {EditImageDialog} from './EditImageDialog'
28import {ImageAltTextDialog} from './ImageAltTextDialog'
···148 const enableSquareButtons = useEnableSquareButtons()
149150 const onImageEdit = () => {
151+ if (IS_NATIVE) {
152 cropImage(image).then(next => {
153 onChange(next)
154 })
···67import {languageName} from '#/locale/helpers'
8import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
9-import {isNative, isWeb} from '#/platform/detection'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {
12 toPostLanguages,
···22import * as Toggle from '#/components/forms/Toggle'
23import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
24import {Text} from '#/components/Typography'
02526export function PostLanguageSelectDialog({
27 control,
···171172 const listHeader = (
173 <View
174- style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]}
175 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
176 <View style={[a.flex_row, a.w_full, a.justify_between]}>
177 <View>
···198 </Text>
199 </View>
200201- {isWeb && (
202 <Button
203 variant="ghost"
204 size="small"
···255 ListHeaderComponent={listHeader}
256 stickyHeaderIndices={[0]}
257 contentContainerStyle={[a.gap_0]}
258- style={[isNative && a.px_lg, web({paddingBottom: 120})]}
259 scrollIndicatorInsets={{top: headerHeight}}
260 renderItem={({item, index}) => {
261 if (item.type === 'header') {
···67import {languageName} from '#/locale/helpers'
8import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
09import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {
11 toPostLanguages,
···21import * as Toggle from '#/components/forms/Toggle'
22import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
23import {Text} from '#/components/Typography'
24+import {IS_NATIVE, IS_WEB} from '#/env'
2526export function PostLanguageSelectDialog({
27 control,
···171172 const listHeader = (
173 <View
174+ style={[a.pb_xs, t.atoms.bg, IS_NATIVE && a.pt_2xl]}
175 onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
176 <View style={[a.flex_row, a.w_full, a.justify_between]}>
177 <View>
···198 </Text>
199 </View>
200201+ {IS_WEB && (
202 <Button
203 variant="ghost"
204 size="small"
···255 ListHeaderComponent={listHeader}
256 stickyHeaderIndices={[0]}
257 contentContainerStyle={[a.gap_0]}
258+ style={[IS_NATIVE && a.px_lg, web({paddingBottom: 120})]}
259 scrollIndicatorInsets={{top: headerHeight}}
260 renderItem={({item, index}) => {
261 if (item.type === 'header') {
+6-4
src/view/com/composer/text-input/TextInput.tsx
···14import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
15import PasteInput, {
16 type PastedFile,
17- type PasteInputRef, // @ts-expect-error no types when installing from github
0018} from '@mattermost/react-native-paste-input'
1920import {POST_IMG_MAX} from '#/lib/constants'
···23import {cleanError} from '#/lib/strings/errors'
24import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
25import {useTheme} from '#/lib/ThemeContext'
26-import {isAndroid, isNative} from '#/platform/detection'
27import {
28 type LinkFacetMatch,
29 suggestLinkCardUri,
30} from '#/view/com/composer/text-input/text-input-util'
31import {atoms as a, useAlf} from '#/alf'
32import {normalizeTextStyles} from '#/alf/typography'
033import {Autocomplete} from './mobile/Autocomplete'
34import {type TextInputProps} from './TextInput.types'
35···226 /**
227 * PasteInput doesn't like `lineHeight`, results in jumpiness
228 */
229- if (isNative) {
230 style.lineHeight = undefined
231 }
232233 /*
234 * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant`
235 */
236- if (isAndroid) {
237 // @ts-ignore
238 style.fontVariant = style.fontVariant
239 ? style.fontVariant.join(' ')
···14import {AppBskyRichtextFacet, RichText, UnicodeString} from '@atproto/api'
15import PasteInput, {
16 type PastedFile,
17+ type PasteInputRef,
18+ // @ts-expect-error no types when installing from github
19+ // eslint-disable-next-line import-x/no-unresolved
20} from '@mattermost/react-native-paste-input'
2122import {POST_IMG_MAX} from '#/lib/constants'
···25import {cleanError} from '#/lib/strings/errors'
26import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
27import {useTheme} from '#/lib/ThemeContext'
028import {
29 type LinkFacetMatch,
30 suggestLinkCardUri,
31} from '#/view/com/composer/text-input/text-input-util'
32import {atoms as a, useAlf} from '#/alf'
33import {normalizeTextStyles} from '#/alf/typography'
34+import {IS_ANDROID, IS_NATIVE} from '#/env'
35import {Autocomplete} from './mobile/Autocomplete'
36import {type TextInputProps} from './TextInput.types'
37···228 /**
229 * PasteInput doesn't like `lineHeight`, results in jumpiness
230 */
231+ if (IS_NATIVE) {
232 style.lineHeight = undefined
233 }
234235 /*
236 * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant`
237 */
238+ if (IS_ANDROID) {
239 // @ts-ignore
240 style.fontVariant = style.fontVariant
241 ? style.fontVariant.join(' ')
···89import {isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11-import {isNative} from '#/platform/detection'
12import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
13import {createPostgateRecord} from '#/state/queries/postgate/util'
14import {usePreferencesQuery} from '#/state/queries/preferences'
···25import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
26import * as Tooltip from '#/components/Tooltip'
27import {Text} from '#/components/Typography'
028import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
2930export function ThreadgateBtn({
···70 nudged: tooltipWasShown,
71 })
7273- if (isNative && Keyboard.isVisible()) {
74 Keyboard.dismiss()
75 }
76
···89import {isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
011import {usePostInteractionSettingsMutation} from '#/state/queries/post-interaction-settings'
12import {createPostgateRecord} from '#/state/queries/postgate/util'
13import {usePreferencesQuery} from '#/state/queries/preferences'
···24import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Group'
25import * as Tooltip from '#/components/Tooltip'
26import {Text} from '#/components/Typography'
27+import {IS_NATIVE} from '#/env'
28import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged'
2930export function ThreadgateBtn({
···70 nudged: tooltipWasShown,
71 })
7273+ if (IS_NATIVE && Keyboard.isVisible()) {
74 Keyboard.dismiss()
75 }
76
+10-6
src/view/com/composer/videos/SubtitleDialog.tsx
···6import {MAX_ALT_TEXT} from '#/lib/constants'
7import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
8import {LANGUAGES} from '#/locale/languages'
9-import {isWeb} from '#/platform/detection'
10import {useLanguagePrefs} from '#/state/preferences'
11import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
12import {atoms as a, useTheme, web} from '#/alf'
···18import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
19import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
20import {Text} from '#/components/Typography'
021import {SubtitleFilePicker} from './SubtitleFilePicker'
2223const MAX_NUM_CAPTIONS = 1
···38 return (
39 <View style={[a.flex_row, a.my_xs]}>
40 <Button
41- label={isWeb ? _(msg`Captions & alt text`) : _(msg`Alt text`)}
42 accessibilityHint={
43- isWeb
44 ? _(msg`Opens captions and alt text dialog`)
45 : _(msg`Opens alt text dialog`)
46 }
···53 }}>
54 <ButtonIcon icon={CCIcon} />
55 <ButtonText>
56- {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>}
000057 </ButtonText>
58 </Button>
59 <Dialog.Outer control={control}>
···135 </Text>
136 )}
137138- {isWeb && (
139 <>
140 <View
141 style={[
···183 <View style={web([a.flex_row, a.justify_end])}>
184 <Button
185 label={_(msg`Done`)}
186- size={isWeb ? 'small' : 'large'}
187 color="primary"
188 variant="solid"
189 onPress={() => {
···6import {MAX_ALT_TEXT} from '#/lib/constants'
7import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
8import {LANGUAGES} from '#/locale/languages'
09import {useLanguagePrefs} from '#/state/preferences'
10import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
11import {atoms as a, useTheme, web} from '#/alf'
···17import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
18import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
19import {Text} from '#/components/Typography'
20+import {IS_WEB} from '#/env'
21import {SubtitleFilePicker} from './SubtitleFilePicker'
2223const MAX_NUM_CAPTIONS = 1
···38 return (
39 <View style={[a.flex_row, a.my_xs]}>
40 <Button
41+ label={IS_WEB ? _(msg`Captions & alt text`) : _(msg`Alt text`)}
42 accessibilityHint={
43+ IS_WEB
44 ? _(msg`Opens captions and alt text dialog`)
45 : _(msg`Opens alt text dialog`)
46 }
···53 }}>
54 <ButtonIcon icon={CCIcon} />
55 <ButtonText>
56+ {IS_WEB ? (
57+ <Trans>Captions & alt text</Trans>
58+ ) : (
59+ <Trans>Alt text</Trans>
60+ )}
61 </ButtonText>
62 </Button>
63 <Dialog.Outer control={control}>
···139 </Text>
140 )}
141142+ {IS_WEB && (
143 <>
144 <View
145 style={[
···187 <View style={web([a.flex_row, a.justify_end])}>
188 <Button
189 label={_(msg`Done`)}
190+ size={IS_WEB ? 'small' : 'large'}
191 color="primary"
192 variant="solid"
193 onPress={() => {
···4import {type ImagePickerAsset} from 'expo-image-picker'
56import {clamp} from '#/lib/numbers'
7-import {isWeb} from '#/platform/detection'
8import {atoms as a, useTheme} from '#/alf'
09import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
10import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
11···20}) {
21 const t = useTheme()
2223- if (isWeb) return null
2425 let aspectRatio = asset.width / asset.height
26
···4import {type ImagePickerAsset} from 'expo-image-picker'
56import {clamp} from '#/lib/numbers'
07import {atoms as a, useTheme} from '#/alf'
8+import {IS_WEB} from '#/env'
9import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
10import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop'
11···20}) {
21 const t = useTheme()
2223+ if (IS_WEB) return null
2425 let aspectRatio = asset.width / asset.height
26
+5-5
src/view/com/feeds/ComposerPrompt.tsx
···11} from '#/lib/hooks/usePermissions'
12import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
13import {logger} from '#/logger'
14-import {isNative} from '#/platform/detection'
15import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
16import {MAX_IMAGES} from '#/view/com/composer/state/composer'
17import {UserAvatar} from '#/view/com/util/UserAvatar'
···22import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
23import {SubtleHover} from '#/components/SubtleHover'
24import {Text} from '#/components/Typography'
02526export function ComposerPrompt() {
27 const {_} = useLingui()
···43 logger.metric('composerPrompt:gallery:press', {})
4445 // On web, open the composer with the gallery picker auto-opening
46- if (!isNative) {
47 openComposer({openGallery: true})
48 return
49 }
···105 return
106 }
107108- if (isNative && Keyboard.isVisible()) {
109 Keyboard.dismiss()
110 }
111···122 ]
123124 openComposer({
125- imageUris: isNative ? imageUris : undefined,
126 })
127 } catch (err: any) {
128 if (!String(err).toLowerCase().includes('cancel')) {
···189 <Trans>What's up?</Trans>
190 </Text>
191 <View style={[a.flex_row, a.gap_md]}>
192- {isNative && (
193 <Button
194 onPress={e => {
195 e.stopPropagation()
···11} from '#/lib/hooks/usePermissions'
12import {openCamera, openUnifiedPicker} from '#/lib/media/picker'
13import {logger} from '#/logger'
014import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
15import {MAX_IMAGES} from '#/view/com/composer/state/composer'
16import {UserAvatar} from '#/view/com/util/UserAvatar'
···21import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image'
22import {SubtleHover} from '#/components/SubtleHover'
23import {Text} from '#/components/Typography'
24+import {IS_NATIVE} from '#/env'
2526export function ComposerPrompt() {
27 const {_} = useLingui()
···43 logger.metric('composerPrompt:gallery:press', {})
4445 // On web, open the composer with the gallery picker auto-opening
46+ if (!IS_NATIVE) {
47 openComposer({openGallery: true})
48 return
49 }
···105 return
106 }
107108+ if (IS_NATIVE && Keyboard.isVisible()) {
109 Keyboard.dismiss()
110 }
111···122 ]
123124 openComposer({
125+ imageUris: IS_NATIVE ? imageUris : undefined,
126 })
127 } catch (err: any) {
128 if (!String(err).toLowerCase().includes('cancel')) {
···189 <Trans>What's up?</Trans>
190 </Text>
191 <View style={[a.flex_row, a.gap_md]}>
192+ {IS_NATIVE && (
193 <Button
194 onPress={e => {
195 e.stopPropagation()
+4-4
src/view/com/feeds/FeedPage.tsx
···20import {type AllNavigatorParams} from '#/lib/routes/types'
21import {logEvent} from '#/lib/statsig/statsig'
22import {s} from '#/lib/styles'
23-import {isNative} from '#/platform/detection'
24import {listenSoftReset} from '#/state/events'
25import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
26import {useSetHomeBadge} from '#/state/home-badge'
···34import {useSession} from '#/state/session'
35import {useSetMinimalShellMode} from '#/state/shell'
36import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
037import {PostFeed} from '../posts/PostFeed'
38import {FAB} from '../util/fab/FAB'
39import {type ListMethods} from '../util/List'
···80 const feedIsVideoMode =
81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
83- return isNative && _isVideoFeed
84 }, [feedInfo])
8586 useEffect(() => {
···9192 const scrollToTop = useCallback(() => {
93 scrollElRef.current?.scrollToOffset({
94- animated: isNative,
95 offset: -headerOffset,
96 })
97 setMinimalShellMode(false)
···136 })
137 }, [scrollToTop, feed, queryClient])
138139- const shouldPrefetch = isNative && isPageAdjacent
140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
141 return (
142 <View
···20import {type AllNavigatorParams} from '#/lib/routes/types'
21import {logEvent} from '#/lib/statsig/statsig'
22import {s} from '#/lib/styles'
023import {listenSoftReset} from '#/state/events'
24import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
25import {useSetHomeBadge} from '#/state/home-badge'
···33import {useSession} from '#/state/session'
34import {useSetMinimalShellMode} from '#/state/shell'
35import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
36+import {IS_NATIVE} from '#/env'
37import {PostFeed} from '../posts/PostFeed'
38import {FAB} from '../util/fab/FAB'
39import {type ListMethods} from '../util/List'
···80 const feedIsVideoMode =
81 feedInfo.contentMode === AppBskyFeedDefs.CONTENTMODEVIDEO
82 const _isVideoFeed = isBskyVideoFeed || feedIsVideoMode
83+ return IS_NATIVE && _isVideoFeed
84 }, [feedInfo])
8586 useEffect(() => {
···9192 const scrollToTop = useCallback(() => {
93 scrollElRef.current?.scrollToOffset({
94+ animated: IS_NATIVE,
95 offset: -headerOffset,
96 })
97 setMinimalShellMode(false)
···136 })
137 }, [scrollToTop, feed, queryClient])
138139+ const shouldPrefetch = IS_NATIVE && isPageAdjacent
140 const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
141 return (
142 <View
+3-3
src/view/com/feeds/MissingFeed.tsx
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
7-import {isNative, isWeb} from '#/platform/detection'
8import {useModerationOpts} from '#/state/preferences/moderation-opts'
9import {getFeedTypeFromUri} from '#/state/queries/feed'
10import {useProfileQuery} from '#/state/queries/profile'
···15import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
16import * as ProfileCard from '#/components/ProfileCard'
17import {Text} from '#/components/Typography'
01819export function MissingFeed({
20 style,
···83 a.italic,
84 ]}
85 numberOfLines={1}>
86- {isWeb ? (
87 <Trans>Click for information</Trans>
88 ) : (
89 <Trans>Tap for information</Trans>
···205 </>
206 )}
207 </View>
208- {isNative && (
209 <Button
210 label={_(msg`Close`)}
211 onPress={() => control.close()}
···4import {useLingui} from '@lingui/react'
56import {cleanError} from '#/lib/strings/errors'
07import {useModerationOpts} from '#/state/preferences/moderation-opts'
8import {getFeedTypeFromUri} from '#/state/queries/feed'
9import {useProfileQuery} from '#/state/queries/profile'
···14import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
15import * as ProfileCard from '#/components/ProfileCard'
16import {Text} from '#/components/Typography'
17+import {IS_NATIVE, IS_WEB} from '#/env'
1819export function MissingFeed({
20 style,
···83 a.italic,
84 ]}
85 numberOfLines={1}>
86+ {IS_WEB ? (
87 <Trans>Click for information</Trans>
88 ) : (
89 <Trans>Tap for information</Trans>
···205 </>
206 )}
207 </View>
208+ {IS_NATIVE && (
209 <Button
210 label={_(msg`Close`)}
211 onPress={() => control.close()}
+4-4
src/view/com/feeds/ProfileFeedgens.tsx
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23-import {isIOS, isNative, isWeb} from '#/platform/detection'
24import {usePreferencesQuery} from '#/state/queries/preferences'
25import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
26import {useSession} from '#/state/session'
···33import * as FeedCard from '#/components/FeedCard'
34import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
35import {ListFooter} from '#/components/Lists'
03637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114- animated: isNative,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···194 return (
195 <View
196 style={[
197- (index !== 0 || isWeb) && a.border_t,
198 t.atoms.border_contrast_low,
199 a.px_lg,
200 a.py_lg,
···218 )
219220 useEffect(() => {
221- if (isIOS && enabled && scrollElRef.current) {
222 const nativeTag = findNodeHandle(scrollElRef.current)
223 setScrollViewTag(nativeTag)
224 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
023import {usePreferencesQuery} from '#/state/queries/preferences'
24import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens'
25import {useSession} from '#/state/session'
···32import * as FeedCard from '#/components/FeedCard'
33import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
34import {ListFooter} from '#/components/Lists'
35+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
3637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114+ animated: IS_NATIVE,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···194 return (
195 <View
196 style={[
197+ (index !== 0 || IS_WEB) && a.border_t,
198 t.atoms.border_contrast_low,
199 a.px_lg,
200 a.py_lg,
···218 )
219220 useEffect(() => {
221+ if (IS_IOS && enabled && scrollElRef.current) {
222 const nativeTag = findNodeHandle(scrollElRef.current)
223 setScrollViewTag(nativeTag)
224 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
23-import {isIOS, isNative, isWeb} from '#/platform/detection'
24import {usePreferencesQuery} from '#/state/queries/preferences'
25import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
26import {useSession} from '#/state/session'
···33import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
34import * as ListCard from '#/components/ListCard'
35import {ListFooter} from '#/components/Lists'
03637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114- animated: isNative,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···193 return (
194 <View
195 style={[
196- (index !== 0 || isWeb) && a.border_t,
197 t.atoms.border_contrast_low,
198 a.px_lg,
199 a.py_lg,
···217 )
218219 useEffect(() => {
220- if (isIOS && enabled && scrollElRef.current) {
221 const nativeTag = findNodeHandle(scrollElRef.current)
222 setScrollViewTag(nativeTag)
223 }
···2021import {cleanError} from '#/lib/strings/errors'
22import {logger} from '#/logger'
023import {usePreferencesQuery} from '#/state/queries/preferences'
24import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists'
25import {useSession} from '#/state/session'
···32import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
33import * as ListCard from '#/components/ListCard'
34import {ListFooter} from '#/components/Lists'
35+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
3637const LOADING = {_reactKey: '__loading__'}
38const EMPTY = {_reactKey: '__empty__'}
···111112 const onScrollToTop = useCallback(() => {
113 scrollElRef.current?.scrollToOffset({
114+ animated: IS_NATIVE,
115 offset: -headerOffset,
116 })
117 queryClient.invalidateQueries({queryKey: RQKEY(did)})
···193 return (
194 <View
195 style={[
196+ (index !== 0 || IS_WEB) && a.border_t,
197 t.atoms.border_contrast_low,
198 a.px_lg,
199 a.py_lg,
···217 )
218219 useEffect(() => {
220+ if (IS_IOS && enabled && scrollElRef.current) {
221 const nativeTag = findNodeHandle(scrollElRef.current)
222 setScrollViewTag(nativeTag)
223 }
+3-3
src/view/com/modals/DeleteAccount.tsx
···16import {cleanError} from '#/lib/strings/errors'
17import {colors, gradients, s} from '#/lib/styles'
18import {useTheme} from '#/lib/ThemeContext'
19-import {isAndroid, isWeb} from '#/platform/detection'
20import {useModalControls} from '#/state/modals'
21import {useAgent, useSession, useSessionApi} from '#/state/session'
22import {pdsAgent} from '#/state/session/agent'
23import {atoms as a, useTheme as useNewTheme} from '#/alf'
24import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
25import {Text as NewText} from '#/components/Typography'
026import {resetToTab} from '../../../Navigation'
27import {ErrorMessage} from '../util/error/ErrorMessage'
28import {Text} from '../util/text/Text'
29import * as Toast from '../util/Toast'
30import {ScrollView, TextInput} from './util'
3132-export const snapPoints = isAndroid ? ['90%'] : ['55%']
3334export function Component({}: {}) {
35 const pal = usePalette('default')
···174 </>
175 )}
176177- <View style={[!isWeb && a.px_xl]}>
178 <View
179 style={[
180 a.w_full,
···16import {cleanError} from '#/lib/strings/errors'
17import {colors, gradients, s} from '#/lib/styles'
18import {useTheme} from '#/lib/ThemeContext'
019import {useModalControls} from '#/state/modals'
20import {useAgent, useSession, useSessionApi} from '#/state/session'
21import {pdsAgent} from '#/state/session/agent'
22import {atoms as a, useTheme as useNewTheme} from '#/alf'
23import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
24import {Text as NewText} from '#/components/Typography'
25+import {IS_ANDROID, IS_WEB} from '#/env'
26import {resetToTab} from '../../../Navigation'
27import {ErrorMessage} from '../util/error/ErrorMessage'
28import {Text} from '../util/text/Text'
29import * as Toast from '../util/Toast'
30import {ScrollView, TextInput} from './util'
3132+export const snapPoints = IS_ANDROID ? ['90%'] : ['55%']
3334export function Component({}: {}) {
35 const pal = usePalette('default')
···174 </>
175 )}
176177+ <View style={[!IS_WEB && a.px_xl]}>
178 <View
179 style={[
180 a.w_full,
···1import React, {useContext} from 'react'
2import {type SharedValue} from 'react-native-reanimated'
34-import {isNative} from '#/platform/detection'
56export const PagerHeaderContext = React.createContext<{
7 scrollY: SharedValue<number>
···3738export function usePagerHeaderContext() {
39 const ctx = useContext(PagerHeaderContext)
40- if (isNative) {
41 if (!ctx) {
42 throw new Error(
43 'usePagerHeaderContext must be used within a HeaderProvider',
···1import React, {useContext} from 'react'
2import {type SharedValue} from 'react-native-reanimated'
34+import {IS_NATIVE} from '#/env'
56export const PagerHeaderContext = React.createContext<{
7 scrollY: SharedValue<number>
···3738export function usePagerHeaderContext() {
39 const ctx = useContext(PagerHeaderContext)
40+ if (IS_NATIVE) {
41 if (!ctx) {
42 throw new Error(
43 'usePagerHeaderContext must be used within a HeaderProvider',
+3-3
src/view/com/pager/PagerWithHeader.tsx
···1819import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
20import {ScrollProvider} from '#/lib/ScrollContext'
21-import {isIOS} from '#/platform/detection'
22import {
23 Pager,
24 type PagerRef,
25 type RenderTabBarFnProps,
26} from '#/view/com/pager/Pager'
27import {useTheme} from '#/alf'
028import {type ListMethods} from '../util/List'
29import {PagerHeaderProvider} from './PagerHeaderContext'
30import {TabBar} from './TabBar'
···273 const headerRef = useRef(null)
274 return (
275 <Animated.View
276- pointerEvents={isIOS ? 'auto' : 'box-none'}
277 style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
278 <View
279 ref={headerRef}
280- pointerEvents={isIOS ? 'auto' : 'box-none'}
281 collapsable={false}>
282 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})}
283 {
···1819import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
20import {ScrollProvider} from '#/lib/ScrollContext'
021import {
22 Pager,
23 type PagerRef,
24 type RenderTabBarFnProps,
25} from '#/view/com/pager/Pager'
26import {useTheme} from '#/alf'
27+import {IS_IOS} from '#/env'
28import {type ListMethods} from '../util/List'
29import {PagerHeaderProvider} from './PagerHeaderContext'
30import {TabBar} from './TabBar'
···273 const headerRef = useRef(null)
274 return (
275 <Animated.View
276+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
277 style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
278 <View
279 ref={headerRef}
280+ pointerEvents={IS_IOS ? 'auto' : 'box-none'}
281 collapsable={false}>
282 {renderHeader?.({setMinimumHeight: setMinimumHeaderHeight})}
283 {
+2-2
src/view/com/posts/CustomFeedEmptyState.tsx
···13import {type NavigationProp} from '#/lib/routes/types'
14import {s} from '#/lib/styles'
15import {logger} from '#/logger'
16-import {isWeb} from '#/platform/detection'
17import {useFeedFeedbackContext} from '#/state/feed-feedback'
18import {useSession} from '#/state/session'
019import {Button} from '../util/forms/Button'
20import {Text} from '../util/text/Text'
21···44 const navigation = useNavigation<NavigationProp>()
4546 const onPressFindAccounts = React.useCallback(() => {
47- if (isWeb) {
48 navigation.navigate('Search', {})
49 } else {
50 navigation.navigate('SearchTab')
···13import {type NavigationProp} from '#/lib/routes/types'
14import {s} from '#/lib/styles'
15import {logger} from '#/logger'
016import {useFeedFeedbackContext} from '#/state/feed-feedback'
17import {useSession} from '#/state/session'
18+import {IS_WEB} from '#/env'
19import {Button} from '../util/forms/Button'
20import {Text} from '../util/text/Text'
21···44 const navigation = useNavigation<NavigationProp>()
4546 const onPressFindAccounts = React.useCallback(() => {
47+ if (IS_WEB) {
48 navigation.navigate('Search', {})
49 } else {
50 navigation.navigate('SearchTab')
+2-2
src/view/com/posts/FollowingEmptyState.tsx
···11import {MagnifyingGlassIcon} from '#/lib/icons'
12import {type NavigationProp} from '#/lib/routes/types'
13import {s} from '#/lib/styles'
14-import {isWeb} from '#/platform/detection'
15import {Button} from '../util/forms/Button'
16import {Text} from '../util/text/Text'
17···21 const navigation = useNavigation<NavigationProp>()
2223 const onPressFindAccounts = React.useCallback(() => {
24- if (isWeb) {
25 navigation.navigate('Search', {})
26 } else {
27 navigation.navigate('SearchTab')
···11import {MagnifyingGlassIcon} from '#/lib/icons'
12import {type NavigationProp} from '#/lib/routes/types'
13import {s} from '#/lib/styles'
14+import {IS_WEB} from '#/env'
15import {Button} from '../util/forms/Button'
16import {Text} from '../util/text/Text'
17···21 const navigation = useNavigation<NavigationProp>()
2223 const onPressFindAccounts = React.useCallback(() => {
24+ if (IS_WEB) {
25 navigation.navigate('Search', {})
26 } else {
27 navigation.navigate('SearchTab')
+2-2
src/view/com/posts/FollowingEndOfFeed.tsx
···10import {usePalette} from '#/lib/hooks/usePalette'
11import {type NavigationProp} from '#/lib/routes/types'
12import {s} from '#/lib/styles'
13-import {isWeb} from '#/platform/detection'
14import {Button} from '../util/forms/Button'
15import {Text} from '../util/text/Text'
16···20 const navigation = useNavigation<NavigationProp>()
2122 const onPressFindAccounts = React.useCallback(() => {
23- if (isWeb) {
24 navigation.navigate('Search', {})
25 } else {
26 navigation.navigate('SearchTab')
···10import {usePalette} from '#/lib/hooks/usePalette'
11import {type NavigationProp} from '#/lib/routes/types'
12import {s} from '#/lib/styles'
13+import {IS_WEB} from '#/env'
14import {Button} from '../util/forms/Button'
15import {Text} from '../util/text/Text'
16···20 const navigation = useNavigation<NavigationProp>()
2122 const onPressFindAccounts = React.useCallback(() => {
23+ if (IS_WEB) {
24 navigation.navigate('Search', {})
25 } else {
26 navigation.navigate('SearchTab')
+4-5
src/view/com/posts/PostFeed.tsx
···34import {logEvent, useGate} from '#/lib/statsig/statsig'
35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
37-import {isIOS, isNative, isWeb} from '#/platform/detection'
38import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
39import {listenPostCreated} from '#/state/events'
40import {useFeedFeedbackContext} from '#/state/feed-feedback'
···72} from '#/components/feeds/PostFeedVideoGridRow'
73import {TrendingInterstitial} from '#/components/interstitials/Trending'
74import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
075import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
76import {ComposerPrompt} from '../feeds/ComposerPrompt'
77import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
···314 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|')
315 const {gtMobile} = useBreakpoints()
316 const {rightNavVisible} = useLayoutBreakpoints()
317- const areVideoFeedsEnabled = isNative
318319 const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState(
320 () => new Set<string>(),
···514 for (const page of data.pages) {
515 for (const slice of page.slices) {
516 const item = slice.items.find(
517- // eslint-disable-next-line @typescript-eslint/no-shadow
518 item => item.uri === slice.feedPostUri,
519 )
520 if (
···986 * reach the end, so that content isn't cut off by the bottom of the
987 * screen.
988 */
989- const offset = Math.max(headerOffset, 32) * (isWeb ? 1 : 2)
990991 return isFetchingNextPage ? (
992 <View style={[styles.feedFooter]}>
···1137 }
1138 initialNumToRender={initialNumToRenderOverride ?? initialNumToRender}
1139 windowSize={9}
1140- maxToRenderPerBatch={isIOS ? 5 : 1}
1141 updateCellsBatchingPeriod={40}
1142 onItemSeen={onItemSeen}
1143 />
···34import {logEvent, useGate} from '#/lib/statsig/statsig'
35import {isNetworkError} from '#/lib/strings/errors'
36import {logger} from '#/logger'
037import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
38import {listenPostCreated} from '#/state/events'
39import {useFeedFeedbackContext} from '#/state/feed-feedback'
···71} from '#/components/feeds/PostFeedVideoGridRow'
72import {TrendingInterstitial} from '#/components/interstitials/Trending'
73import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
74+import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
75import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
76import {ComposerPrompt} from '../feeds/ComposerPrompt'
77import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
···314 const [feedType, feedUriOrActorDid, feedTab] = feed.split('|')
315 const {gtMobile} = useBreakpoints()
316 const {rightNavVisible} = useLayoutBreakpoints()
317+ const areVideoFeedsEnabled = IS_NATIVE
318319 const [hasPressedShowLessUris, setHasPressedShowLessUris] = useState(
320 () => new Set<string>(),
···514 for (const page of data.pages) {
515 for (const slice of page.slices) {
516 const item = slice.items.find(
0517 item => item.uri === slice.feedPostUri,
518 )
519 if (
···985 * reach the end, so that content isn't cut off by the bottom of the
986 * screen.
987 */
988+ const offset = Math.max(headerOffset, 32) * (IS_WEB ? 1 : 2)
989990 return isFetchingNextPage ? (
991 <View style={[styles.feedFooter]}>
···1136 }
1137 initialNumToRender={initialNumToRenderOverride ?? initialNumToRender}
1138 windowSize={9}
1139+ maxToRenderPerBatch={IS_IOS ? 5 : 1}
1140 updateCellsBatchingPeriod={40}
1141 onItemSeen={onItemSeen}
1142 />
+2-2
src/view/com/profile/ProfileFollows.tsx
···8import {type NavigationProp} from '#/lib/routes/types'
9import {cleanError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11-import {isWeb} from '#/platform/detection'
12import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
13import {useResolveDidQuery} from '#/state/queries/resolve-uri'
14import {useSession} from '#/state/session'
15import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX'
16import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
17import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
018import {List} from '../util/List'
19import {ProfileCardWithFollowBtn} from './ProfileCard'
20···49 const navigation = useNavigation<NavigationProp>()
5051 const onPressFindAccounts = React.useCallback(() => {
52- if (isWeb) {
53 navigation.navigate('Search', {})
54 } else {
55 navigation.navigate('SearchTab')
···8import {type NavigationProp} from '#/lib/routes/types'
9import {cleanError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
011import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
12import {useResolveDidQuery} from '#/state/queries/resolve-uri'
13import {useSession} from '#/state/session'
14import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX'
15import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
16import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
17+import {IS_WEB} from '#/env'
18import {List} from '../util/List'
19import {ProfileCardWithFollowBtn} from './ProfileCard'
20···49 const navigation = useNavigation<NavigationProp>()
5051 const onPressFindAccounts = React.useCallback(() => {
52+ if (IS_WEB) {
53 navigation.navigate('Search', {})
54 } else {
55 navigation.navigate('SearchTab')
+10-8
src/view/com/profile/ProfileMenu.tsx
···12import {shareText, shareUrl} from '#/lib/sharing'
13import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
15-import {isWeb} from '#/platform/detection'
16import {type Shadow} from '#/state/cache/types'
17import {useModalControls} from '#/state/modals'
18import {
···67import {useFullVerificationState} from '#/components/verification'
68import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
69import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
070import {Dot} from '#/features/nuxs/components/Dot'
71import {Gradient} from '#/features/nuxs/components/Gradient'
72import {useDevMode} from '#/storage/hooks/dev-mode'
···283 <Menu.Item
284 testID="profileHeaderDropdownShareBtn"
285 label={
286- isWeb ? _(msg`Copy link to profile`) : _(msg`Share via...`)
287 }
288 onPress={() => {
289 if (showLoggedOutWarning) {
···293 }
294 }}>
295 <Menu.ItemText>
296- {isWeb ? (
297 <Trans>Copy link to profile</Trans>
298 ) : (
299 <Trans>Share via...</Trans>
300 )}
301 </Menu.ItemText>
302- <Menu.ItemIcon icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} />
00303 </Menu.Item>
304 <Menu.Item
305 testID="profileHeaderDropdownShareBtn"
306 label={
307- isWeb
308 ? _(msg`Copy via bsky.app`)
309 : _(msg`Share via bsky.app...`)
310 }
···316 }
317 }}>
318 <Menu.ItemText>
319- {isWeb ? (
320 <Trans>Copy via bsky.app</Trans>
321 ) : (
322 <Trans>Share via bsky.app...</Trans>
323 )}
324 </Menu.ItemText>
325- <Menu.ItemIcon icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} />
326 </Menu.Item>
327 <Menu.Item
328 testID="profileHeaderDropdownSearchBtn"
···453 a.flex_0,
454 {
455 color: t.palette.primary_500,
456- right: isWeb ? -8 : -4,
457 },
458 ]}>
459 <Trans>New</Trans>
···12import {shareText, shareUrl} from '#/lib/sharing'
13import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
14import {logger} from '#/logger'
015import {type Shadow} from '#/state/cache/types'
16import {useModalControls} from '#/state/modals'
17import {
···66import {useFullVerificationState} from '#/components/verification'
67import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt'
68import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
69+import {IS_WEB} from '#/env'
70import {Dot} from '#/features/nuxs/components/Dot'
71import {Gradient} from '#/features/nuxs/components/Gradient'
72import {useDevMode} from '#/storage/hooks/dev-mode'
···283 <Menu.Item
284 testID="profileHeaderDropdownShareBtn"
285 label={
286+ IS_WEB ? _(msg`Copy link to profile`) : _(msg`Share via...`)
287 }
288 onPress={() => {
289 if (showLoggedOutWarning) {
···293 }
294 }}>
295 <Menu.ItemText>
296+ {IS_WEB ? (
297 <Trans>Copy link to profile</Trans>
298 ) : (
299 <Trans>Share via...</Trans>
300 )}
301 </Menu.ItemText>
302+ <Menu.ItemIcon
303+ icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon}
304+ />
305 </Menu.Item>
306 <Menu.Item
307 testID="profileHeaderDropdownShareBtn"
308 label={
309+ IS_WEB
310 ? _(msg`Copy via bsky.app`)
311 : _(msg`Share via bsky.app...`)
312 }
···318 }
319 }}>
320 <Menu.ItemText>
321+ {IS_WEB ? (
322 <Trans>Copy via bsky.app</Trans>
323 ) : (
324 <Trans>Share via bsky.app...</Trans>
325 )}
326 </Menu.ItemText>
327+ <Menu.ItemIcon icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon} />
328 </Menu.Item>
329 <Menu.Item
330 testID="profileHeaderDropdownSearchBtn"
···455 a.flex_0,
456 {
457 color: t.palette.primary_500,
458+ right: IS_WEB ? -8 : -4,
459 },
460 ]}>
461 <Trans>New</Trans>
+4-2
src/view/com/testing/TestCtrls.e2e.tsx
···1import {useState} from 'react'
2-import {LogBox, Pressable, View, TextInput} from 'react-native'
3import {useQueryClient} from '@tanstack/react-query'
45import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
6-import {useSessionApi, useAgent} from '#/state/session'
7import {useLoggedOutViewControls} from '#/state/shell/logged-out'
8import {useOnboardingDispatch} from '#/state/shell/onboarding'
9import {navigate} from '../../../Navigation'
···50 return (
51 <View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
52 <TextInput
0053 testID="e2eProxyHeaderInput"
54 onChangeText={val => setProxyHeader(val as any)}
55 onSubmitEditing={() => {
···1import {useState} from 'react'
2+import {LogBox, Pressable, TextInput, View} from 'react-native'
3import {useQueryClient} from '@tanstack/react-query'
45import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
6+import {useAgent, useSessionApi} from '#/state/session'
7import {useLoggedOutViewControls} from '#/state/shell/logged-out'
8import {useOnboardingDispatch} from '#/state/shell/onboarding'
9import {navigate} from '../../../Navigation'
···50 return (
51 <View style={{position: 'absolute', top: 100, right: 0, zIndex: 100}}>
52 <TextInput
53+ accessibilityLabel="Text input field"
54+ accessibilityHint="Enter proxy header"
55 testID="e2eProxyHeaderInput"
56 onChangeText={val => setProxyHeader(val as any)}
57 onSubmitEditing={() => {
···25 linkRequiresWarning,
26} from '#/lib/strings/url-helpers'
27import {type TypographyVariant} from '#/lib/ThemeContext'
28-import {isAndroid, isWeb} from '#/platform/detection'
29import {emitSoftReset} from '#/state/events'
30import {useModalControls} from '#/state/modals'
31import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
32import {useTheme} from '#/alf'
33import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
034import {router} from '../../../routes'
35import {PressableWithHover} from './PressableWithHover'
36import {Text} from './text/Text'
···130 android_ripple={{
131 color: t.atoms.bg_contrast_25.backgroundColor,
132 }}
133- unstable_pressDelay={isAndroid ? 90 : undefined}>
134 {/* @ts-ignore web only -prf */}
135 <View style={style} href={anchorHref}>
136 {children ? children : <Text>{title || 'link'}</Text>}
···219 })
220 }
221 if (
222- isWeb &&
223 href !== '#' &&
224 e != null &&
225 isModifiedEvent(e as React.MouseEvent)
···323 onBeforePress,
324 ...props
325}: TextLinkOnWebOnlyProps) {
326- if (isWeb) {
327 return (
328 <TextLink
329 testID={testID}
···25 linkRequiresWarning,
26} from '#/lib/strings/url-helpers'
27import {type TypographyVariant} from '#/lib/ThemeContext'
028import {emitSoftReset} from '#/state/events'
29import {useModalControls} from '#/state/modals'
30import {WebAuxClickWrapper} from '#/view/com/util/WebAuxClickWrapper'
31import {useTheme} from '#/alf'
32import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
33+import {IS_ANDROID, IS_WEB} from '#/env'
34import {router} from '../../../routes'
35import {PressableWithHover} from './PressableWithHover'
36import {Text} from './text/Text'
···130 android_ripple={{
131 color: t.atoms.bg_contrast_25.backgroundColor,
132 }}
133+ unstable_pressDelay={IS_ANDROID ? 90 : undefined}>
134 {/* @ts-ignore web only -prf */}
135 <View style={style} href={anchorHref}>
136 {children ? children : <Text>{title || 'link'}</Text>}
···219 })
220 }
221 if (
222+ IS_WEB &&
223 href !== '#' &&
224 e != null &&
225 isModifiedEvent(e as React.MouseEvent)
···323 onBeforePress,
324 ...props
325}: TextLinkOnWebOnlyProps) {
326+ if (IS_WEB) {
327 return (
328 <TextLink
329 testID={testID}
+3-3
src/view/com/util/List.tsx
···11import {useDedupe} from '#/lib/hooks/useDedupe'
12import {useScrollHandlers} from '#/lib/ScrollContext'
13import {addStyle} from '#/lib/styles'
14-import {isIOS} from '#/platform/detection'
15import {useLightbox} from '#/state/lightbox'
16import {useTheme} from '#/alf'
017import {FlatList_INTERNAL} from './Views'
1819export type ListMethods = FlatList_INTERNAL
···94 }
95 }
9697- if (isIOS) {
98 runOnJS(dedupe)(updateActiveVideoViewAsync)
99 }
100 },
···184185// We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only
186// removing it saves us a re-render on Android
187-const useAllowScrollToTop = isIOS ? useAllowScrollToTopIOS : () => undefined
188function useAllowScrollToTopIOS() {
189 const {activeLightbox} = useLightbox()
190 return !activeLightbox
···11import {useDedupe} from '#/lib/hooks/useDedupe'
12import {useScrollHandlers} from '#/lib/ScrollContext'
13import {addStyle} from '#/lib/styles'
014import {useLightbox} from '#/state/lightbox'
15import {useTheme} from '#/alf'
16+import {IS_IOS} from '#/env'
17import {FlatList_INTERNAL} from './Views'
1819export type ListMethods = FlatList_INTERNAL
···94 }
95 }
9697+ if (IS_IOS) {
98 runOnJS(dedupe)(updateActiveVideoViewAsync)
99 }
100 },
···184185// We only want to use this context value on iOS because the `scrollsToTop` prop is iOS-only
186// removing it saves us a re-render on Android
187+const useAllowScrollToTop = IS_IOS ? useAllowScrollToTopIOS : () => undefined
188function useAllowScrollToTopIOS() {
189 const {activeLightbox} = useLightbox()
190 return !activeLightbox
···5import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
6import {usePalette} from '#/lib/hooks/usePalette'
7import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
8-import {isWeb} from '#/platform/detection'
9import {atoms as a} from '#/alf'
010import {Text} from '../text/Text'
1112export const LoggedOutLayout = ({
···79 contentContainerStyle={styles.scrollViewContentContainer}
80 keyboardShouldPersistTaps="handled"
81 keyboardDismissMode="on-drag">
82- <View style={[styles.contentWrapper, isWeb && a.my_auto]}>
83 {children}
84 </View>
85 </ScrollView>
···5import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
6import {usePalette} from '#/lib/hooks/usePalette'
7import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
08import {atoms as a} from '#/alf'
9+import {IS_WEB} from '#/env'
10import {Text} from '../text/Text'
1112export const LoggedOutLayout = ({
···79 contentContainerStyle={styles.scrollViewContentContainer}
80 keyboardShouldPersistTaps="handled"
81 keyboardDismissMode="on-drag">
82+ <View style={[styles.contentWrapper, IS_WEB && a.my_auto]}>
83 {children}
84 </View>
85 </ScrollView>
+4-3
src/view/com/util/text/Text.tsx
···5import {lh, s} from '#/lib/styles'
6import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
7import {logger} from '#/logger'
8-import {isIOS, isWeb} from '#/platform/detection'
9import {applyFonts, useAlf} from '#/alf'
10import {
11 childHasEmoji,
12 renderChildrenWithEmoji,
13 type StringChild,
14} from '#/alf/typography'
01516export type CustomTextProps = Omit<TextProps, 'children'> & {
17 type?: TypographyVariant
···51 if (__DEV__) {
52 if (!emoji && childHasEmoji(children)) {
53 logger.warn(
054 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
55 )
56 }
···80 }
8182 return {
83- uiTextView: selectable && isIOS,
84 selectable,
85 style: flattened,
86- dataSet: isWeb
87 ? Object.assign({tooltip: title}, dataSet || {})
88 : undefined,
89 ...props,
···5import {lh, s} from '#/lib/styles'
6import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
7import {logger} from '#/logger'
08import {applyFonts, useAlf} from '#/alf'
9import {
10 childHasEmoji,
11 renderChildrenWithEmoji,
12 type StringChild,
13} from '#/alf/typography'
14+import {IS_IOS, IS_WEB} from '#/env'
1516export type CustomTextProps = Omit<TextProps, 'children'> & {
17 type?: TypographyVariant
···51 if (__DEV__) {
52 if (!emoji && childHasEmoji(children)) {
53 logger.warn(
54+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
55 `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
56 )
57 }
···81 }
8283 return {
84+ uiTextView: selectable && IS_IOS,
85 selectable,
86 style: flattened,
87+ dataSet: IS_WEB
88 ? Object.assign({tooltip: title}, dataSet || {})
89 : undefined,
90 ...props,
+4-4
src/view/screens/Feeds.tsx
···16} from '#/lib/routes/types'
17import {cleanError} from '#/lib/strings/errors'
18import {s} from '#/lib/styles'
19-import {isNative, isWeb} from '#/platform/detection'
20import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
21import {
22 type SavedFeedItem,
···47import * as Layout from '#/components/Layout'
48import {Link} from '#/components/Link'
49import * as ListCard from '#/components/ListCard'
05051type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
52···388 const onChangeSearchFocus = React.useCallback(
389 (focus: boolean) => {
390 if (focus && searchBarIndex > -1) {
391- if (isNative) {
392 // scrollToIndex scrolls the exact right amount, so use if available
393 listRef.current?.scrollToIndex({
394 index: searchBarIndex,
···684 return (
685 <View
686 style={
687- isWeb
688 ? [
689 a.flex_row,
690 a.px_md,
···720 return (
721 <View
722 style={
723- isWeb
724 ? [a.flex_row, a.px_md, a.pt_lg, a.pb_lg, a.gap_md]
725 : [{flexDirection: 'row-reverse'}, a.p_lg, a.gap_md]
726 }>
···16} from '#/lib/routes/types'
17import {cleanError} from '#/lib/strings/errors'
18import {s} from '#/lib/styles'
019import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
20import {
21 type SavedFeedItem,
···46import * as Layout from '#/components/Layout'
47import {Link} from '#/components/Link'
48import * as ListCard from '#/components/ListCard'
49+import {IS_NATIVE, IS_WEB} from '#/env'
5051type Props = NativeStackScreenProps<CommonNavigatorParams, 'Feeds'>
52···388 const onChangeSearchFocus = React.useCallback(
389 (focus: boolean) => {
390 if (focus && searchBarIndex > -1) {
391+ if (IS_NATIVE) {
392 // scrollToIndex scrolls the exact right amount, so use if available
393 listRef.current?.scrollToIndex({
394 index: searchBarIndex,
···684 return (
685 <View
686 style={
687+ IS_WEB
688 ? [
689 a.flex_row,
690 a.px_md,
···720 return (
721 <View
722 style={
723+ IS_WEB
724 ? [a.flex_row, a.px_md, a.pt_lg, a.pb_lg, a.gap_md]
725 : [{flexDirection: 'row-reverse'}, a.p_lg, a.gap_md]
726 }>
+2-2
src/view/screens/Home.tsx
···12 type NativeStackScreenProps,
13} from '#/lib/routes/types'
14import {logEvent} from '#/lib/statsig/statsig'
15-import {isWeb} from '#/platform/detection'
16import {emitSoftReset} from '#/state/events'
17import {
18 type SavedFeedSourceInfo,
···37import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
38import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
39import * as Layout from '#/components/Layout'
040import {useDemoMode} from '#/storage/hooks/demo-mode'
4142type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
···48 usePinnedFeedsInfos()
4950 React.useEffect(() => {
51- if (isWeb && !currentAccount) {
52 const getParams = new URLSearchParams(window.location.search)
53 const splash = getParams.get('splash')
54 if (splash === 'true') {
···12 type NativeStackScreenProps,
13} from '#/lib/routes/types'
14import {logEvent} from '#/lib/statsig/statsig'
015import {emitSoftReset} from '#/state/events'
16import {
17 type SavedFeedSourceInfo,
···36import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed'
37import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
38import * as Layout from '#/components/Layout'
39+import {IS_WEB} from '#/env'
40import {useDemoMode} from '#/storage/hooks/demo-mode'
4142type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
···48 usePinnedFeedsInfos()
4950 React.useEffect(() => {
51+ if (IS_WEB && !currentAccount) {
52 const getParams = new URLSearchParams(window.location.search)
53 const splash = getParams.get('splash')
54 if (splash === 'true') {
+3-3
src/view/screens/Notifications.tsx
···14} from '#/lib/routes/types'
15import {s} from '#/lib/styles'
16import {logger} from '#/logger'
17-import {isNative} from '#/platform/detection'
18import {emitSoftReset, listenSoftReset} from '#/state/events'
19import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
20import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
···40import * as Layout from '#/components/Layout'
41import {InlineLinkText, Link} from '#/components/Link'
42import {Loader} from '#/components/Loader'
04344// We don't currently persist this across reloads since
45// you gotta visit All to clear the badge anyway.
···200 // event handlers
201 // =
202 const scrollToTop = useCallback(() => {
203- scrollElRef.current?.scrollToOffset({animated: isNative, offset: 0})
204 setMinimalShellMode(false)
205 }, [scrollElRef, setMinimalShellMode])
206···230 // on focus, check for latest, but only invalidate if the user
231 // isnt scrolled down to avoid moving content underneath them
232 let currentIsScrolledDown
233- if (isNative) {
234 currentIsScrolledDown = isScrolledDown
235 } else {
236 // On the web, this isn't always updated in time so
···14} from '#/lib/routes/types'
15import {s} from '#/lib/styles'
16import {logger} from '#/logger'
017import {emitSoftReset, listenSoftReset} from '#/state/events'
18import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
19import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
···39import * as Layout from '#/components/Layout'
40import {InlineLinkText, Link} from '#/components/Link'
41import {Loader} from '#/components/Loader'
42+import {IS_NATIVE} from '#/env'
4344// We don't currently persist this across reloads since
45// you gotta visit All to clear the badge anyway.
···200 // event handlers
201 // =
202 const scrollToTop = useCallback(() => {
203+ scrollElRef.current?.scrollToOffset({animated: IS_NATIVE, offset: 0})
204 setMinimalShellMode(false)
205 }, [scrollElRef, setMinimalShellMode])
206···230 // on focus, check for latest, but only invalidate if the user
231 // isnt scrolled down to avoid moving content underneath them
232 let currentIsScrolledDown
233+ if (IS_NATIVE) {
234 currentIsScrolledDown = isScrolledDown
235 } else {
236 // On the web, this isn't always updated in time so
+2-2
src/view/shell/Drawer.tsx
···13import {type NavigationProp} from '#/lib/routes/types'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {colors} from '#/lib/styles'
16-import {isWeb} from '#/platform/detection'
17import {emitSoftReset} from '#/state/events'
18import {useDisableFollowersMetrics} from '#/state/preferences/disable-followers-metrics'
19import {useDisableFollowingMetrics} from '#/state/preferences/disable-following-metrics'
···58import {Text} from '#/components/Typography'
59import {useSimpleVerificationState} from '#/components/verification'
60import {VerificationCheck} from '#/components/verification/VerificationCheck'
06162const iconWidth = 26
63···183 (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => {
184 const state = navigation.getState()
185 setDrawerOpen(false)
186- if (isWeb) {
187 // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
188 if (tab === 'MyProfile') {
189 navigation.navigate('Profile', {name: currentAccount!.handle})
···13import {type NavigationProp} from '#/lib/routes/types'
14import {sanitizeHandle} from '#/lib/strings/handles'
15import {colors} from '#/lib/styles'
016import {emitSoftReset} from '#/state/events'
17import {useDisableFollowersMetrics} from '#/state/preferences/disable-followers-metrics'
18import {useDisableFollowingMetrics} from '#/state/preferences/disable-following-metrics'
···57import {Text} from '#/components/Typography'
58import {useSimpleVerificationState} from '#/components/verification'
59import {VerificationCheck} from '#/components/verification/VerificationCheck'
60+import {IS_WEB} from '#/env'
6162const iconWidth = 26
63···183 (tab: 'Home' | 'Search' | 'Messages' | 'Notifications' | 'MyProfile') => {
184 const state = navigation.getState()
185 setDrawerOpen(false)
186+ if (IS_WEB) {
187 // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
188 if (tab === 'MyProfile') {
189 navigation.navigate('Profile', {name: currentAccount!.handle})