enable react native strict types

This commit is contained in:
Samuel Newman
2026-08-13 11:42:09 +03:00
parent 6b35d3de1c
commit 8677387c23
104 changed files with 457 additions and 506 deletions
+10
View File
@@ -56,6 +56,16 @@ jest.mock('expo-media-library', () => ({
__esModule: true, // this property makes it work __esModule: true, // this property makes it work
default: jest.fn(), default: jest.fn(),
usePermissions: jest.fn(() => [true]), usePermissions: jest.fn(() => [true]),
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
}))
jest.mock('expo-media-library/legacy', () => ({
__esModule: true,
default: jest.fn(),
usePermissions: jest.fn(() => [true]),
requestPermissionsAsync: jest.fn().mockResolvedValue({granted: true}),
saveToLibraryAsync: jest.fn().mockResolvedValue(undefined),
})) }))
jest.mock('@bsky.app/expo-guess-language', () => ({ jest.mock('@bsky.app/expo-guess-language', () => ({
+15 -167
View File
@@ -80,14 +80,6 @@
"count": 1 "count": 1
} }
}, },
"src/alf/util/flatten.ts": {
"typescript/no-explicit-any": {
"count": 1
},
"typescript/no-unsafe-member-access": {
"count": 9
}
},
"src/alf/util/systemUI.ts": { "src/alf/util/systemUI.ts": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 2 "count": 2
@@ -167,9 +159,6 @@
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 4 "count": 4
}, },
"typescript/no-misused-promises": {
"count": 1
},
"typescript/no-unsafe-call": { "typescript/no-unsafe-call": {
"count": 2 "count": 2
}, },
@@ -224,9 +213,6 @@
"src/components/Lists.tsx": { "src/components/Lists.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/components/Menu/types.ts": { "src/components/Menu/types.ts": {
@@ -261,9 +247,6 @@
"src/components/PostControls/BookmarkButton.tsx": { "src/components/PostControls/BookmarkButton.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 2 "count": 2
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/components/PostControls/DiscoverDebug.tsx": { "src/components/PostControls/DiscoverDebug.tsx": {
@@ -335,9 +318,6 @@
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 5 "count": 5
}, },
"typescript/no-misused-promises": {
"count": 2
},
"typescript/require-await": { "typescript/require-await": {
"count": 4 "count": 4
} }
@@ -346,9 +326,6 @@
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": {
"count": 2
},
"typescript/require-await": { "typescript/require-await": {
"count": 1 "count": 1
} }
@@ -363,16 +340,6 @@
"count": 1 "count": 1
} }
}, },
"src/components/ageAssurance/AgeAssuranceErrors.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/ageAssurance/AgeAssuranceInitDialog.tsx": {
"typescript/no-misused-promises": {
"count": 2
}
},
"src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": { "src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx": {
"typescript/require-await": { "typescript/require-await": {
"count": 1 "count": 1
@@ -388,18 +355,10 @@
"count": 1 "count": 1
} }
}, },
"src/components/contacts/screens/ViewMatches.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/dialogs/DeviceLocationRequestDialog.tsx": { "src/components/dialogs/DeviceLocationRequestDialog.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": {
"count": 1
},
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 2 "count": 2
} }
@@ -409,26 +368,6 @@
"count": 1 "count": 1
} }
}, },
"src/components/dialogs/EmailDialog/screens/Manage2FA/Disable.tsx": {
"typescript/no-misused-promises": {
"count": 3
}
},
"src/components/dialogs/EmailDialog/screens/Manage2FA/Enable.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/dialogs/EmailDialog/screens/Update.tsx": {
"typescript/no-misused-promises": {
"count": 3
}
},
"src/components/dialogs/EmailDialog/screens/Verify.tsx": {
"typescript/no-misused-promises": {
"count": 3
}
},
"src/components/dialogs/LanguageSelectDialog.tsx": { "src/components/dialogs/LanguageSelectDialog.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
@@ -447,7 +386,7 @@
"count": 2 "count": 2
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 3 "count": 1
}, },
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 2 "count": 2
@@ -490,9 +429,6 @@
"src/components/dialogs/lists/CreateOrEditListDialog.tsx": { "src/components/dialogs/lists/CreateOrEditListDialog.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 2 "count": 2
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": { "src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": {
@@ -580,16 +516,6 @@
"count": 2 "count": 2
} }
}, },
"src/components/intents/VerifyEmailIntentDialog.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/verification/VerificationCreatePrompt.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/components/verification/VerificationRemovePrompt.tsx": { "src/components/verification/VerificationRemovePrompt.tsx": {
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 1 "count": 1
@@ -696,13 +622,10 @@
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 3 "count": 2
} }
}, },
"src/lib/hooks/usePermissions.ts": { "src/lib/hooks/usePermissions.ts": {
"typescript/no-misused-promises": {
"count": 1
},
"typescript/no-unsafe-enum-comparison": { "typescript/no-unsafe-enum-comparison": {
"count": 5 "count": 5
} }
@@ -848,12 +771,12 @@
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 1 "count": 1
},
"typescript/no-unsafe-member-access": {
"count": 1
} }
}, },
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": { "src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
"typescript/no-misused-promises": {
"count": 6
},
"typescript/require-await": { "typescript/require-await": {
"count": 6 "count": 6
} }
@@ -863,22 +786,9 @@
"count": 1 "count": 1
} }
}, },
"src/screens/Feeds/NoSavedFeedsOfAnyType.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/screens/Home/NoFeedsPinned.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/screens/List/ListHiddenScreen.tsx": { "src/screens/List/ListHiddenScreen.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 3 "count": 3
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/screens/Log.tsx": { "src/screens/Log.tsx": {
@@ -894,7 +804,7 @@
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 2 "count": 1
}, },
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 2 "count": 2
@@ -922,19 +832,6 @@
"src/screens/Onboarding/StepProfile/index.tsx": { "src/screens/Onboarding/StepProfile/index.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 2 "count": 2
},
"typescript/no-misused-promises": {
"count": 1
}
},
"src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/screens/Onboarding/StepSuggestedStarterpacks/index.tsx": {
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": { "src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx": {
@@ -962,18 +859,12 @@
"src/screens/Profile/Header/EditProfileDialog.tsx": { "src/screens/Profile/Header/EditProfileDialog.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 3 "count": 3
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/screens/Profile/Header/ProfileHeaderLabeler.tsx": { "src/screens/Profile/Header/ProfileHeaderLabeler.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 2 "count": 2
}, },
"typescript/no-misused-promises": {
"count": 1
},
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 3 "count": 3
} }
@@ -1004,11 +895,6 @@
"count": 1 "count": 1
} }
}, },
"src/screens/ProfileList/components/Header.tsx": {
"typescript/no-misused-promises": {
"count": 3
}
},
"src/screens/ProfileList/components/MoreOptionsMenu.tsx": { "src/screens/ProfileList/components/MoreOptionsMenu.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1027,11 +913,6 @@
"count": 1 "count": 1
} }
}, },
"src/screens/SavedFeeds.tsx": {
"typescript/no-misused-promises": {
"count": 2
}
},
"src/screens/Search/modules/ExploreSuggestedAccounts.tsx": { "src/screens/Search/modules/ExploreSuggestedAccounts.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1096,6 +977,12 @@
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 2 "count": 2
},
"typescript/no-unsafe-call": {
"count": 2
},
"typescript/no-unsafe-member-access": {
"count": 2
} }
}, },
"src/screens/Settings/components/CopyButton.tsx": { "src/screens/Settings/components/CopyButton.tsx": {
@@ -1107,9 +994,6 @@
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": {
"count": 1
},
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 1 "count": 1
} }
@@ -1119,11 +1003,6 @@
"count": 2 "count": 2
} }
}, },
"src/screens/Settings/components/DisableEmail2FADialog.tsx": {
"typescript/no-misused-promises": {
"count": 4
}
},
"src/screens/Settings/components/OTAInfo.tsx": { "src/screens/Settings/components/OTAInfo.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1147,7 +1026,7 @@
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 2 "count": 1
}, },
"typescript/no-unsafe-call": { "typescript/no-unsafe-call": {
"count": 1 "count": 1
@@ -1168,9 +1047,6 @@
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
}, },
"typescript/no-misused-promises": {
"count": 2
},
"typescript/require-await": { "typescript/require-await": {
"count": 1 "count": 1
} }
@@ -1474,9 +1350,6 @@
"src/view/com/composer/SelectMediaButton.tsx": { "src/view/com/composer/SelectMediaButton.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/view/com/composer/drafts/DraftsButton.tsx": { "src/view/com/composer/drafts/DraftsButton.tsx": {
@@ -1500,11 +1373,6 @@
"count": 2 "count": 2
} }
}, },
"src/view/com/composer/photos/EditImageDialog.web.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/view/com/composer/photos/Gallery.tsx": { "src/view/com/composer/photos/Gallery.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1513,9 +1381,6 @@
"src/view/com/composer/photos/OpenCameraBtn.tsx": { "src/view/com/composer/photos/OpenCameraBtn.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
},
"typescript/no-misused-promises": {
"count": 1
} }
}, },
"src/view/com/composer/text-input/TextInput.web.tsx": { "src/view/com/composer/text-input/TextInput.web.tsx": {
@@ -1560,7 +1425,7 @@
"count": 2 "count": 2
}, },
"typescript/no-misused-promises": { "typescript/no-misused-promises": {
"count": 3 "count": 2
}, },
"typescript/no-unsafe-member-access": { "typescript/no-unsafe-member-access": {
"count": 6 "count": 6
@@ -1598,12 +1463,6 @@
"src/view/com/pager/TabBar.web.tsx": { "src/view/com/pager/TabBar.web.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
},
"typescript/no-unsafe-call": {
"count": 2
},
"typescript/no-unsafe-member-access": {
"count": 4
} }
}, },
"src/view/com/post-thread/PostLikedBy.tsx": { "src/view/com/post-thread/PostLikedBy.tsx": {
@@ -1627,9 +1486,6 @@
"src/view/com/posts/FeedShutdownMsg.tsx": { "src/view/com/posts/FeedShutdownMsg.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 2 "count": 2
},
"typescript/no-misused-promises": {
"count": 2
} }
}, },
"src/view/com/posts/PostFeedErrorMessage.tsx": { "src/view/com/posts/PostFeedErrorMessage.tsx": {
@@ -1693,9 +1549,6 @@
"src/view/com/util/Views.tsx": { "src/view/com/util/Views.tsx": {
"react/display-name": { "react/display-name": {
"count": 1 "count": 1
},
"typescript/no-explicit-any": {
"count": 1
} }
}, },
"src/view/com/util/Views.web.tsx": { "src/view/com/util/Views.web.tsx": {
@@ -1703,11 +1556,6 @@
"count": 1 "count": 1
} }
}, },
"src/view/com/util/forms/Button.tsx": {
"typescript/no-misused-promises": {
"count": 1
}
},
"src/view/screens/Feeds.tsx": { "src/view/screens/Feeds.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 3 "count": 3
@@ -1771,4 +1619,4 @@
"count": 3 "count": 3
} }
} }
} }
+3 -2
View File
@@ -24,10 +24,11 @@ import {atoms as a} from '#/alf'
import splashImagePointer from '../assets/splash/splash.png' import splashImagePointer from '../assets/splash/splash.png'
// @ts-ignore // @ts-ignore
import darkSplashImagePointer from '../assets/splash/splash-dark.png' import darkSplashImagePointer from '../assets/splash/splash-dark.png'
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer)!.uri
const darkSplashImageUri = RNImage.resolveAssetSource( const darkSplashImageUri = RNImage.resolveAssetSource(
darkSplashImagePointer, darkSplashImagePointer,
).uri )!.uri
export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) { export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
const width = 1000 const width = 1000
+2 -2
View File
@@ -7,7 +7,7 @@
import {useEffect, useRef, useState} from 'react' import {useEffect, useRef, useState} from 'react'
import Svg, {Path} from 'react-native-svg' import Svg, {Path} from 'react-native-svg'
import {atoms as a, flatten} from '#/alf' import {atoms as a, flattenToCSS} from '#/alf'
const size = 100 const size = 100
const ratio = 57 / 64 const ratio = 57 / 64
@@ -72,7 +72,7 @@ export function Splash({
{!isAnimationComplete && ( {!isAnimationComplete && (
<div <div
ref={splashRef} ref={splashRef}
style={flatten([ style={flattenToCSS([
a.fixed, a.fixed,
a.inset_0, a.inset_0,
a.flex, a.flex,
+6 -1
View File
@@ -3,6 +3,8 @@ import {type FontVariant, type TextStyle} from 'react-native'
import {IS_ANDROID, IS_WEB} from '#/env' import {IS_ANDROID, IS_WEB} from '#/env'
import {type Device, device} from '#/storage' import {type Device, device} from '#/storage'
export type MutableTextStyle = {-readonly [K in keyof TextStyle]: TextStyle[K]}
const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"` const WEB_FONT_FAMILIES = `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
const factor = 0.0625 // 1 - (15/16) const factor = 0.0625 // 1 - (15/16)
@@ -37,7 +39,10 @@ export function setFontFamily(fontFamily: Device['fontFamily']) {
/* /*
* Unused fonts are commented out, but the files are there if we need them. * Unused fonts are commented out, but the files are there if we need them.
*/ */
export function applyFonts(style: TextStyle, fontFamily: 'system' | 'theme') { export function applyFonts(
style: MutableTextStyle,
fontFamily: 'system' | 'theme',
) {
if (fontFamily === 'theme') { if (fontFamily === 'theme') {
if (IS_ANDROID) { if (IS_ANDROID) {
style.fontFamily = style.fontFamily =
+8 -2
View File
@@ -7,7 +7,13 @@ import {
import {UITextView} from '@bsky.app/react-native-uitextview' import {UITextView} from '@bsky.app/react-native-uitextview'
import createEmojiRegex from 'emoji-regex' import createEmojiRegex from 'emoji-regex'
import {type Alf, applyFonts, atoms, flatten} from '#/alf' import {
type Alf,
applyFonts,
atoms,
flatten,
type MutableTextStyle,
} from '#/alf'
import {IS_IOS, IS_NATIVE} from '#/env' import {IS_IOS, IS_NATIVE} from '#/env'
/** /**
@@ -27,7 +33,7 @@ export function normalizeTextStyles(
fontFamily: Alf['fonts']['family'] fontFamily: Alf['fonts']['family']
} & Pick<Alf, 'flags'>, } & Pick<Alf, 'flags'>,
) { ) {
const s = flatten(styles) ?? {} const s: MutableTextStyle = {...flatten(styles)}
// should always be defined on these components // should always be defined on these components
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
+7 -4
View File
@@ -1,5 +1,5 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import {Dimensions} from 'react-native' import {Dimensions, type DimensionsPayload} from 'react-native'
/** /**
* Same as `useWindowDimensions().fontScale`, but avoids rerendering * Same as `useWindowDimensions().fontScale`, but avoids rerendering
@@ -9,9 +9,12 @@ export function useNativeFontScale() {
const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale) const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale)
useEffect(() => { useEffect(() => {
const sub = Dimensions.addEventListener('change', evt => { const sub = Dimensions.addEventListener(
setFontScale(evt.window.fontScale) 'change',
}) (evt: DimensionsPayload) => {
if (evt.window) setFontScale(evt.window.fontScale)
},
)
return () => sub.remove() return () => sub.remove()
}, []) }, [])
+16 -3
View File
@@ -1,6 +1,19 @@
import {type DimensionValue, StyleSheet} from 'react-native' import {type DimensionValue, type StyleProp, StyleSheet} from 'react-native'
export const flatten = StyleSheet.flatten export function flatten<T>(
style?: StyleProp<T>,
): T extends (infer U)[] ? U : T {
return (StyleSheet.flatten(
style as unknown as Parameters<typeof StyleSheet.flatten>[0],
) ?? {}) as T extends (infer U)[] ? U : T
}
/** Flatten React Native styles passed directly to a web-only DOM component. */
export function flattenToCSS(style: unknown): React.CSSProperties {
return (StyleSheet.flatten(
style as Parameters<typeof StyleSheet.flatten>[0],
) ?? {}) as React.CSSProperties
}
/** /**
* Coerce a style value to a number. Padding values are typed as * Coerce a style value to a number. Padding values are typed as
@@ -28,7 +41,7 @@ interface PaddingStyle {
* non-numeric `DimensionValue` (e.g. percentages) is treated as 0. * non-numeric `DimensionValue` (e.g. percentages) is treated as 0.
*/ */
export function extractPadding(style: PaddingStyle | PaddingStyle[]) { export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
const s = flatten(style as any) ?? {} const s = flatten(style)
const base = num(s.padding) const base = num(s.padding)
return { return {
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base, paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
+1 -1
View File
@@ -24,7 +24,7 @@ export function useThemeName(): ThemeName {
} }
function getThemeName( function getThemeName(
colorScheme: ColorSchemeName, colorScheme: ColorSchemeName | null | undefined,
colorMode: 'system' | 'light' | 'dark', colorMode: 'system' | 'light' | 'dark',
darkTheme?: ThemeName, darkTheme?: ThemeName,
) { ) {
+3 -3
View File
@@ -1,9 +1,9 @@
import {Pressable} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_20} from '#/lib/constants' import {HITSLOP_20} from '#/lib/constants'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Pressable} from '#/components/Pressable'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -49,7 +49,7 @@ export function AltBadgeWithDialog({
accessibilityHint="" accessibilityHint=""
hitSlop={HITSLOP_20} hitSlop={HITSLOP_20}
onPress={control.open} onPress={control.open}
style={s => [ style={({pressed, hovered}) => [
a.justify_center, a.justify_center,
a.rounded_sm, a.rounded_sm,
a.p_xs, a.p_xs,
@@ -62,7 +62,7 @@ export function AltBadgeWithDialog({
opacity: 0.8, opacity: 0.8,
}, },
pos, pos,
s.hovered || s.pressed hovered || pressed
? [ ? [
{ {
opacity: 1, opacity: 1,
+2 -1
View File
@@ -1,11 +1,12 @@
import {useState} from 'react' import {useState} from 'react'
import {type Insets, Pressable, View} from 'react-native' import {type Insets, View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
import {Pressable} from '#/components/Pressable'
import * as Tooltip from '#/components/Tooltip' import * as Tooltip from '#/components/Tooltip'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
+3 -1
View File
@@ -101,6 +101,8 @@ export type ButtonProps = Pick<
| 'onPressOut' | 'onPressOut'
| 'onFocus' | 'onFocus'
| 'onBlur' | 'onBlur'
| 'onAccessibilityAction'
| 'onAccessibilityEscape'
> & > &
AccessibilityProps & AccessibilityProps &
VariantProps & { VariantProps & {
@@ -131,7 +133,7 @@ export function useButtonContext() {
return useContext(Context) return useContext(Context)
} }
export const Button = forwardRef<View, ButtonProps>( export const Button = forwardRef<React.ComponentRef<typeof View>, ButtonProps>(
( (
{ {
children, children,
+11 -3
View File
@@ -39,6 +39,9 @@ import {
import {Span, Text} from '#/components/Typography' import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env' import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
type TextInputInstance = React.ComponentRef<typeof TextInput>
type ViewInstance = React.ComponentRef<typeof View>
export type SubmitRequest = export type SubmitRequest =
| { | {
platform: 'web' platform: 'web'
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
input?: ReturnType<typeof useTapper>['input'] input?: ReturnType<typeof useTapper>['input']
clear: () => void clear: () => void
insert(text: string): void insert(text: string): void
setAutocompleteAnchor: (node: View | null) => void setAutocompleteAnchor: (node: ViewInstance | null) => void
} }
export function useComposerInternalApiRef() { export function useComposerInternalApiRef() {
@@ -82,7 +85,7 @@ export type ComposerProps = Omit<
| 'onSubmitEditing' | 'onSubmitEditing'
> & { > & {
label: string label: string
ref?: React.RefObject<TextInput> ref?: React.RefObject<TextInputInstance>
internalApiRef?: React.Ref<ComposerInternalApi> internalApiRef?: React.Ref<ComposerInternalApi>
outerStyle?: ViewStyleProp['style'] outerStyle?: ViewStyleProp['style']
contentTextStyle?: TextStyleProp['style'] contentTextStyle?: TextStyleProp['style']
@@ -139,6 +142,11 @@ export function Composer({
placement: autocompletePlacement, placement: autocompletePlacement,
dynamicWidth: IS_WEB, dynamicWidth: IS_WEB,
}) })
const inputRef = mergeRefs<TextInputInstance>([
ref,
tapper.inputProps.ref as React.Ref<TextInputInstance>,
sift.targetProps.ref as React.Ref<TextInputInstance>,
])
/* /*
* Active facet state for controlling the visibility of the Autocomplete. * Active facet state for controlling the visibility of the Autocomplete.
@@ -345,7 +353,7 @@ export function Composer({
{...rest} {...rest}
{...tapper.inputProps} {...tapper.inputProps}
{...sift.targetProps} {...sift.targetProps}
ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])} ref={inputRef}
rawValue={tapper.state.text} rawValue={tapper.state.text}
onBlur={e => { onBlur={e => {
rest.onBlur?.(e) rest.onBlur?.(e)
+5 -2
View File
@@ -244,7 +244,7 @@ export function Trigger({
const context = useContextMenuContext() const context = useContextMenuContext()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const ref = useRef<View>(null) const ref = useRef<React.ComponentRef<typeof View>>(null)
const isFocused = useIsFocused() const isFocused = useIsFocused()
const [image, setImage] = useState<string | null>(null) const [image, setImage] = useState<string | null>(null)
const [pendingMeasurement, setPendingMeasurement] = useState<{ const [pendingMeasurement, setPendingMeasurement] = useState<{
@@ -971,7 +971,10 @@ export function Divider() {
) )
} }
function measureView(view: View | null, insets: EdgeInsets) { function measureView(
view: React.ComponentRef<typeof View> | null,
insets: EdgeInsets,
) {
if (!view) return Promise.resolve(null) if (!view) return Promise.resolve(null)
return new Promise<Measurement>(resolve => { return new Promise<Measurement>(resolve => {
view?.measureInWindow((x, y, width, height) => view?.measureInWindow((x, y, width, height) =>
+76 -71
View File
@@ -8,7 +8,7 @@ import {
} from 'react' } from 'react'
import { import {
Keyboard, Keyboard,
type KeyboardEventListener, type KeyboardEvent,
type LayoutChangeEvent, type LayoutChangeEvent,
type NativeScrollEvent, type NativeScrollEvent,
type NativeSyntheticEvent, type NativeSyntheticEvent,
@@ -203,80 +203,85 @@ export function Inner(props: DialogInnerProps) {
return <ScrollableInner {...props} /> return <ScrollableInner {...props} />
} }
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>( export function ScrollableInner({
function ScrollableInner( ref,
{children, contentContainerStyle, header, footer, style, ...props}, children,
ref, contentContainerStyle,
) { header,
const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} = footer,
useDialogContext() style,
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full ...props
const insets = useSafeAreaInsets() }: DialogInnerProps & {
const [keyboardHeight, setKeyboardHeight] = useState(() => ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0, }) {
) const {nativeSnapPoint, disableDrag, setDisableDrag, isHeightConstrained} =
useDialogContext()
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = useState(() =>
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
)
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => { const keyboardEventHandler = useCallback((e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height) setKeyboardHeight(e.endCoordinates.height)
}, []) }, [])
useOnKeyboard('keyboardDidShow', keyboardEventHandler) useOnKeyboard('keyboardDidShow', keyboardEventHandler)
useOnKeyboard('keyboardDidHide', keyboardEventHandler) useOnKeyboard('keyboardDidHide', keyboardEventHandler)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!IS_ANDROID) { if (!IS_ANDROID) {
return return
}
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
} }
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
}
return ( return (
<> <>
<ScrollView <ScrollView
style={[isHeightConstrained && a.flex_1, style]} style={[isHeightConstrained && a.flex_1, style]}
contentContainerStyle={[ contentContainerStyle={[
a.pt_2xl, a.pt_2xl,
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl, IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
platform({ platform({
ios: a.pb_2xl, ios: a.pb_2xl,
android: { android: {
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl, paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
}, },
}), }),
contentContainerStyle, contentContainerStyle,
]} ]}
ref={ref} ref={ref}
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
contentInsetAdjustmentBehavior={ contentInsetAdjustmentBehavior={
isAtMaxSnapPoint ? 'automatic' : 'never' isAtMaxSnapPoint ? 'automatic' : 'never'
} }
automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint} automaticallyAdjustKeyboardInsets={isAtMaxSnapPoint}
{...props} {...props}
bounces={isAtMaxSnapPoint} bounces={isAtMaxSnapPoint}
scrollEventThrottle={50} scrollEventThrottle={50}
// set drag state based on scroll on android. // set drag state based on scroll on android.
// we want to detect if it's at the top or not, so watch // we want to detect if it's at the top or not, so watch
// scrollEndDrag and momentumScrollEnd as well // scrollEndDrag and momentumScrollEnd as well
onScroll={android(onScroll)} onScroll={android(onScroll)}
onScrollEndDrag={android(onScroll)} onScrollEndDrag={android(onScroll)}
onMomentumScrollEnd={android(onScroll)} onMomentumScrollEnd={android(onScroll)}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
// TODO: figure out why this positions the header absolutely (rather than stickily) // TODO: figure out why this positions the header absolutely (rather than stickily)
// on Android. fine to disable for now, because we don't have any // on Android. fine to disable for now, because we don't have any
// dialogs that use this that actually scroll -sfn // dialogs that use this that actually scroll -sfn
stickyHeaderIndices={ios(header ? [0] : undefined)}> stickyHeaderIndices={ios(header ? [0] : undefined)}>
{header} {header}
{children} {children}
</ScrollView> </ScrollView>
{footer} {footer}
</> </>
) )
}, }
)
export const InnerFlatList = forwardRef< export const InnerFlatList = forwardRef<
ListMethods, ListMethods,
+3 -2
View File
@@ -193,7 +193,6 @@ export function Inner({
aria-label={label} aria-label={label}
aria-labelledby={accessibilityLabelledBy} aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy} aria-describedby={accessibilityDescribedBy}
// @ts-expect-error web only -prf
onClick={stopPropagation} onClick={stopPropagation}
onStartShouldSetResponder={_ => true} onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation} onTouchEnd={stopPropagation}
@@ -239,7 +238,9 @@ export function Inner({
export function ScrollableInner({ export function ScrollableInner({
ref: _ref, ref: _ref,
...props ...props
}: DialogInnerProps & {ref?: React.Ref<ScrollView>}) { }: DialogInnerProps & {
ref?: React.Ref<React.ComponentRef<typeof ScrollView>>
}) {
return <Inner {...props} /> return <Inner {...props} />
} }
+1 -1
View File
@@ -217,7 +217,7 @@ export function ProfileGrid({
// Track seen profiles // Track seen profiles
const seenProfilesRef = useRef<Set<string>>(new Set()) const seenProfilesRef = useRef<Set<string>>(new Set())
const containerRef = useRef<View>(null) const containerRef = useRef<React.ComponentRef<typeof View>>(null)
const hasTrackedRef = useRef(false) const hasTrackedRef = useRef(false)
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
? 'DiscoverInterstitial' ? 'DiscoverInterstitial'
+11 -8
View File
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
*/ */
function FocusTrap({children}: {children: React.ReactNode}) { function FocusTrap({children}: {children: React.ReactNode}) {
const {_} = useLingui() const {_} = useLingui()
const child = useRef<View>(null) const child = useRef<React.ComponentRef<typeof View>>(null)
/* /*
* Here we add a ref to the first child of this component. This currently * Here we add a ref to the first child of this component. This currently
@@ -66,13 +66,16 @@ function FocusTrap({children}: {children: React.ReactNode}) {
}) })
}, [children]) }, [children])
const focusNode = useCallback((ref: View | null) => { const focusNode = useCallback(
if (!ref) return (ref: React.ComponentRef<typeof View> | null) => {
const node = findNodeHandle(ref) if (!ref) return
if (node) { const node = findNodeHandle(ref)
AccessibilityInfo.setAccessibilityFocus(node) if (node) {
} AccessibilityInfo.setAccessibilityFocus(node)
}, []) }
},
[],
)
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
+1 -1
View File
@@ -20,7 +20,7 @@ export const IS_GLASS_AVAILABLE =
*/ */
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
export type GlassViewProps = ExpoGlassViewProps & { export type GlassViewProps = Omit<ExpoGlassViewProps, 'ref'> & {
fallbackStyle?: StyleProp<ViewStyle> fallbackStyle?: StyleProp<ViewStyle>
} }
+1 -1
View File
@@ -47,7 +47,7 @@ export function InterestTabs({
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const listRef = useRef<ScrollView>(null) const listRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const [totalWidth, setTotalWidth] = useState(0) const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0) const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0) const [contentWidth, setContentWidth] = useState(0)
+1 -1
View File
@@ -42,7 +42,7 @@ export function Outer({
}: { }: {
children: React.ReactNode children: React.ReactNode
noBottomBorder?: boolean noBottomBorder?: boolean
headerRef?: React.RefObject<View | null> headerRef?: React.RefObject<React.ComponentRef<typeof View> | null>
sticky?: boolean sticky?: boolean
}) { }) {
const t = useTheme() const t = useTheme()
+1 -1
View File
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
export function ImageMenu({onPressShare, onPressSave}: Props) { export function ImageMenu({onPressShare, onPressSave}: Props) {
const {t: l} = useLingui() const {t: l} = useLingui()
const triggerRef = useRef<View>(null) const triggerRef = useRef<React.ComponentRef<typeof View>>(null)
const [isMounted, setIsMounted] = useState(false) const [isMounted, setIsMounted] = useState(false)
const [anchor, setAnchor] = useState<Anchor | null>(null) const [anchor, setAnchor] = useState<Anchor | null>(null)
const progress = useSharedValue(0) const progress = useSharedValue(0)
@@ -82,7 +82,7 @@ const ImageItem = ({
const scrollHandler = useAnimatedScrollHandler({ const scrollHandler = useAnimatedScrollHandler({
onScroll(e) { onScroll(e) {
'worklet' 'worklet'
const nextIsScaled = e.zoomScale > 1 const nextIsScaled = (e.zoomScale ?? 1) > 1
if (scaled !== nextIsScaled) { if (scaled !== nextIsScaled) {
scheduleOnRN(handleZoom, nextIsScaled) scheduleOnRN(handleZoom, nextIsScaled)
} }
+6 -3
View File
@@ -11,7 +11,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native' import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge' import {SystemBars} from 'react-native-edge-to-edge'
import {Gesture} from 'react-native-gesture-handler' import {Gesture} from 'react-native-gesture-handler'
import PagerView from 'react-native-pager-view' import PagerView, {
type PagerViewOnPageSelectedEvent,
type PageScrollStateChangedNativeEvent,
} from 'react-native-pager-view'
import Animated, { import Animated, {
type AnimatableValue, type AnimatableValue,
type AnimatedRef, type AnimatedRef,
@@ -383,7 +386,7 @@ function ImageView({
<PagerView <PagerView
scrollEnabled={!isScaled} scrollEnabled={!isScaled}
initialPage={initialImageIndex} initialPage={initialImageIndex}
onPageSelected={e => { onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
const next = e.nativeEvent.position const next = e.nativeEvent.position
setImageIndex(prev => { setImageIndex(prev => {
if (metricsContext && prev !== next) { if (metricsContext && prev !== next) {
@@ -401,7 +404,7 @@ function ImageView({
}) })
setIsScaled(false) setIsScaled(false)
}} }}
onPageScrollStateChanged={e => { onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
setIsDragging(e.nativeEvent.pageScrollState !== 'idle') setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
}} }}
overdrag={true} overdrag={true}
+2 -2
View File
@@ -478,7 +478,7 @@ export function InlineLinkText({
onIn: onInteract, onIn: onInteract,
onOut: onInteractOut, onOut: onInteractOut,
} = useInteractionState() } = useInteractionState()
const flattenedStyle = flatten(style) || {} const flattenedStyle = flatten(style)
return ( return (
<Text <Text
@@ -570,7 +570,7 @@ export function SimpleInlineLinkText({
onIn: onInteract, onIn: onInteract,
onOut: onInteractOut, onOut: onInteractOut,
} = useInteractionState() } = useInteractionState()
const flattenedStyle = flatten(style) || {} const flattenedStyle = flatten(style)
const isExternal = isExternalUrl(to) const isExternal = isExternalUrl(to)
let href = to let href = to
+2 -2
View File
@@ -11,7 +11,7 @@ import {useLingui} from '@lingui/react'
import {DropdownMenu} from 'radix-ui' import {DropdownMenu} from 'radix-ui'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useTheme, web} from '#/alf' import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
import type * as Dialog from '#/components/Dialog' import type * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import { import {
@@ -413,7 +413,7 @@ export function Divider() {
const t = useTheme() const t = useTheme()
return ( return (
<DropdownMenu.Separator <DropdownMenu.Separator
style={flatten([ style={flattenToCSS([
a.my_xs, a.my_xs,
t.atoms.bg_contrast_100, t.atoms.bg_contrast_100,
a.flex_shrink_0, a.flex_shrink_0,
+1 -1
View File
@@ -28,7 +28,7 @@ export type RadixPassThroughTriggerProps = {
['aria-controls']?: string ['aria-controls']?: string
['aria-haspopup']?: boolean ['aria-haspopup']?: boolean
['aria-expanded']?: AccessibilityProps['aria-expanded'] ['aria-expanded']?: AccessibilityProps['aria-expanded']
onKeyDown: (e: React.KeyboardEvent) => void onKeyDown: PressableProps['onKeyDown']
/** /**
* Radix provides this, but we override on web to use `onPress` instead, * Radix provides this, but we override on web to use `onPress` instead,
* which is less sensitive while scrolling. * which is less sensitive while scrolling.
+1 -1
View File
@@ -267,7 +267,7 @@ function TranslationResult({
? codeToLanguageName(resultSourceLanguage, i18n.locale) ? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined : undefined
const flattenedStyle = flatten(postTextStyle) ?? {} const flattenedStyle = flatten(postTextStyle)
const fontSize = flattenedStyle.fontSize const fontSize = flattenedStyle.fontSize
return ( return (
@@ -27,7 +27,7 @@ export function PostControlButton({
activeColor, activeColor,
...props ...props
}: Omit<ButtonProps, 'hitSlop'> & { }: Omit<ButtonProps, 'hitSlop'> & {
ref?: React.Ref<View> ref?: React.Ref<React.ComponentRef<typeof View>>
active?: boolean active?: boolean
big?: boolean big?: boolean
color?: string color?: string
+38
View File
@@ -0,0 +1,38 @@
import {
Pressable as NativePressable,
type PressableStateCallbackType as NativePressableStateCallbackType,
type StyleProp,
type ViewStyle,
} from 'react-native'
export interface PressableStateCallbackType extends NativePressableStateCallbackType {
/** Provided by react-native-web. */
readonly focused?: boolean
/** Provided by react-native-web. */
readonly hovered?: boolean
}
export type PressableProps = Omit<
React.ComponentProps<typeof NativePressable>,
'children' | 'style'
> & {
children?:
| React.ReactNode
| ((state: PressableStateCallbackType) => React.ReactNode)
style?:
| StyleProp<ViewStyle>
| ((state: PressableStateCallbackType) => StyleProp<ViewStyle>)
}
/**
* React Native Pressable with react-native-web's callback state represented in
* its types. The web-only fields are optional because native does not provide
* them at runtime.
*/
export function Pressable({children, style, ...props}: PressableProps) {
return (
<NativePressable {...props} style={style}>
{children}
</NativePressable>
)
}
@@ -1,5 +1,5 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {TextInput, View, type ViewToken} from 'react-native' import {type ListViewToken as ViewToken, TextInput, View} from 'react-native'
import {type ModerationOpts} from '@bsky/sdk/moderation' import {type ModerationOpts} from '@bsky/sdk/moderation'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
const [searchText, setSearchText] = useState(lastSearchText) const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [headerHeight, setHeaderHeight] = useState(0) const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession() const {currentAccount} = useSession()
@@ -374,7 +374,7 @@ let Header = ({
interestsDisplayNames, interestsDisplayNames,
}: { }: {
guide?: Follow10ProgressGuide guide?: Follow10ProgressGuide
inputRef: React.RefObject<TextInput | null> inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
listRef: React.RefObject<ListMethods | null> listRef: React.RefObject<ListMethods | null>
onSelectTab: (v: string) => void onSelectTab: (v: string) => void
searchText: string searchText: string
@@ -679,7 +679,7 @@ function SearchInput({
}: { }: {
onChangeText: (text: string) => void onChangeText: (text: string) => void
onEscape: () => void onEscape: () => void
inputRef: React.RefObject<TextInput | null> inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
defaultValue: string defaultValue: string
}) { }) {
const t = useTheme() const t = useTheme()
+1 -1
View File
@@ -93,7 +93,7 @@ export function RichText({
if (!facets?.length) { if (!facets?.length) {
if (isOnlyEmoji(text)) { if (isOnlyEmoji(text)) {
const flattenedStyle = flatten(style) ?? {} const flattenedStyle = flatten(style)
const fontSize = const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier (flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return ( return (
+10 -1
View File
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
} else { } else {
return ( return (
<Button <Button
hitSlop={hitSlop} hitSlop={
typeof hitSlop === 'number'
? {
top: hitSlop,
right: hitSlop,
bottom: hitSlop,
left: hitSlop,
}
: (hitSlop ?? undefined)
}
label={label} label={label}
onPress={control.open} onPress={control.open}
style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]} style={[a.flex_1, a.justify_between, a.pl_lg, a.pr_md]}
+9 -9
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {Select as RadixSelect} from 'radix-ui' import {Select as RadixSelect} from 'radix-ui'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {atoms as a, flatten, useTheme, web} from '#/alf' import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import { import {
@@ -94,7 +94,7 @@ export function Trigger({children, label}: TriggerProps) {
onBlur={onBlur} onBlur={onBlur}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
style={flatten([ style={flattenToCSS([
a.flex, a.flex,
a.relative, a.relative,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
@@ -196,7 +196,7 @@ export function Content<T>({
return ( return (
<RadixSelect.Portal> <RadixSelect.Portal>
<RadixSelect.Content <RadixSelect.Content
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])} style={flattenToCSS([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
position="popper" position="popper"
align="center" align="center"
sideOffset={5} sideOffset={5}
@@ -212,17 +212,17 @@ export function Content<T>({
a.overflow_hidden, a.overflow_hidden,
!reduceMotionEnabled && a.zoom_fade_in, !reduceMotionEnabled && a.zoom_fade_in,
]}> ]}>
<RadixSelect.ScrollUpButton style={flatten(up)}> <RadixSelect.ScrollUpButton style={flattenToCSS(up)}>
<ChevronUpIcon style={[t.atoms.text]} size="xs" /> <ChevronUpIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollUpButton> </RadixSelect.ScrollUpButton>
<RadixSelect.Viewport style={flatten([a.p_xs])}> <RadixSelect.Viewport style={flattenToCSS([a.p_xs])}>
{items.map((item, index) => ( {items.map((item, index) => (
<Fragment key={valueExtractor(item)}> <Fragment key={valueExtractor(item)}>
{renderItem(item, index, selectedValue)} {renderItem(item, index, selectedValue)}
</Fragment> </Fragment>
))} ))}
</RadixSelect.Viewport> </RadixSelect.Viewport>
<RadixSelect.ScrollDownButton style={flatten(down)}> <RadixSelect.ScrollDownButton style={flattenToCSS(down)}>
<ChevronDownIcon style={[t.atoms.text]} size="xs" /> <ChevronDownIcon style={[t.atoms.text]} size="xs" />
</RadixSelect.ScrollDownButton> </RadixSelect.ScrollDownButton>
</View> </View>
@@ -273,7 +273,7 @@ export function Item({ref, value, style, children}: ItemProps) {
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
style={flatten([ style={flattenToCSS([
t.atoms.text, t.atoms.text,
a.relative, a.relative,
a.flex, a.flex,
@@ -307,7 +307,7 @@ export const ItemText = function ItemText({children, style}: ItemTextProps) {
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
return ( return (
<RadixSelect.ItemIndicator <RadixSelect.ItemIndicator
style={flatten([ style={flattenToCSS([
a.absolute, a.absolute,
{left: 0, width: 30}, {left: 0, width: 30},
a.flex, a.flex,
@@ -324,7 +324,7 @@ export function Separator() {
return ( return (
<RadixSelect.Separator <RadixSelect.Separator
style={flatten([ style={flattenToCSS([
{ {
height: 1, height: 1,
backgroundColor: t.atoms.border_contrast_low.borderColor, backgroundColor: t.atoms.border_contrast_low.borderColor,
@@ -2,10 +2,6 @@ import {forwardRef, useCallback, useImperativeHandle, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native' import {type ListRenderItemInfo, View} from 'react-native'
import {AtUri} from '@atproto/syntax' import {AtUri} from '@atproto/syntax'
import {type ModerationOpts} from '@bsky/sdk/moderation' import {type ModerationOpts} from '@bsky/sdk/moderation'
import {
type InfiniteData,
type UseInfiniteQueryResult,
} from '@tanstack/react-query'
import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
@@ -26,9 +22,6 @@ function keyExtractor(item: app.bsky.actor.defs.ProfileView, index: number) {
interface ProfilesListProps { interface ProfilesListProps {
listUri: string listUri: string
listMembersQuery: UseInfiniteQueryResult<
InfiniteData<app.bsky.graph.getList.$OutputBody>
>
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
headerHeight: number headerHeight: number
scrollElRef: ListRef scrollElRef: ListRef
+5 -4
View File
@@ -27,8 +27,9 @@ import {
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const TooltipPortal = createPortalGroup() const TooltipPortal = createPortalGroup()
const TooltipProviderContext = const TooltipProviderContext = createContext<React.RefObject<React.ComponentRef<
createContext<React.RefObject<View | null> | null>(null) typeof View
> | null> | null>(null)
/** /**
* Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container, * Provider for Tooltip component. Only needed when you need to position the tooltip relative to a container,
@@ -37,7 +38,7 @@ const TooltipProviderContext =
* Only really necessary on iOS but can work on Android. * Only really necessary on iOS but can work on Android.
*/ */
export function SheetCompatProvider({children}: {children: React.ReactNode}) { export function SheetCompatProvider({children}: {children: React.ReactNode}) {
const ref = useRef<View | null>(null) const ref = useRef<React.ComponentRef<typeof View> | null>(null)
return ( return (
<GlobalGestureEventsProvider style={[a.flex_1]}> <GlobalGestureEventsProvider style={[a.flex_1]}>
<TooltipPortal.Provider> <TooltipPortal.Provider>
@@ -154,7 +155,7 @@ export function Outer({
export function Target({children}: {children: React.ReactNode}) { export function Target({children}: {children: React.ReactNode}) {
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext) const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const [hasLaidOut, setHasLaidOut] = useState(false) const [hasLaidOut, setHasLaidOut] = useState(false)
const targetRef = useRef<View>(null) const targetRef = useRef<React.ComponentRef<typeof View>>(null)
const containerRef = useContext(TooltipProviderContext) const containerRef = useContext(TooltipProviderContext)
const keyboardIsOpen = useIsKeyboardVisible() const keyboardIsOpen = useIsKeyboardVisible()
+2 -2
View File
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {utils} from '@bsky.app/alf' import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui' import {Popover} from 'radix-ui'
import {atoms as a, flatten, useTheme} from '#/alf' import {atoms as a, flattenToCSS, useTheme} from '#/alf'
import { import {
ARROW_SIZE, ARROW_SIZE,
BUBBLE_MAX_WIDTH, BUBBLE_MAX_WIDTH,
@@ -85,7 +85,7 @@ export function Content({
evt.preventDefault() evt.preventDefault()
} }
}} }}
style={flatten([ style={flattenToCSS([
a.rounded_sm, a.rounded_sm,
{ {
backgroundColor: style.surface, backgroundColor: style.surface,
@@ -25,13 +25,13 @@ export function OTPInput({
label: string label: string
value: string value: string
onChange: (text: string) => void onChange: (text: string) => void
ref?: React.Ref<TextInput> ref?: React.Ref<React.ComponentRef<typeof TextInput>>
numberOfDigits?: number numberOfDigits?: number
onComplete?: (code: string) => void onComplete?: (code: string) => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const innerRef = useRef<TextInput>(null) const innerRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [selection, setSelection] = useState({start: 0, end: 0}) const [selection, setSelection] = useState({start: 0, end: 0})
@@ -383,7 +383,7 @@ export function ViewMatches({
ref={listRef} ref={listRef}
data={items} data={items}
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : null} ListFooterComponent={!isEmpty ? <ListFooter height={20} /> : undefined}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
keyboardDismissMode="interactive" keyboardDismissMode="interactive"
automaticallyAdjustKeyboardInsets automaticallyAdjustKeyboardInsets
@@ -97,7 +97,7 @@ export function SearchablePeopleList({
const [headerHeight, setHeaderHeight] = useState(0) const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
@@ -634,7 +634,7 @@ function SearchInput({
value: string value: string
onChangeText: (text: string) => void onChangeText: (text: string) => void
onEscape: () => void onEscape: () => void
inputRef: React.RefObject<TextInput | null> inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -182,9 +182,9 @@ function ListsContent({
<View style={[a.align_center, a.py_lg]}> <View style={[a.align_center, a.py_lg]}>
<Loader size="lg" /> <Loader size="lg" />
</View> </View>
) : null ) : undefined
} }
ListEmptyComponent={!isLoading && data ? <Empty /> : null} ListEmptyComponent={!isLoading && data ? <Empty /> : undefined}
webInnerContentContainerStyle={[a.py_0]} webInnerContentContainerStyle={[a.py_0]}
style={platform({ style={platform({
web: [a.px_2xl, a.pb_md], web: [a.px_2xl, a.pb_md],
+2 -2
View File
@@ -134,7 +134,7 @@ export function AddMembersFlow({
const [footerHeight, setFooterHeight] = useState(0) const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer( const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
reducer, reducer,
@@ -502,7 +502,7 @@ export function AddMembersFlow({
<View style={[a.flex_1, a.align_center, a.justify_center]}> <View style={[a.flex_1, a.align_center, a.justify_center]}>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>
) : null ) : undefined
} }
keyExtractor={(item: Item) => item.key} keyExtractor={(item: Item) => item.key}
style={[ style={[
+1 -1
View File
@@ -249,7 +249,7 @@ export function InitiateChatFlow({
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const aa = useAgeAssurance() const aa = useAgeAssurance()
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const accountTooNewPromptControl = Dialog.useDialogControl() const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({ const {data: convos} = useListConvosQuery({
+1 -1
View File
@@ -255,7 +255,7 @@ function ReactionTabs({
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const scrollViewRef = useRef<ScrollView>(null) const scrollViewRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const scrollState = useRef({x: 0, width: 0}) const scrollState = useRef({x: 0, width: 0})
const tabLayouts = useRef<Map<string, {x: number; width: number}>>(new Map()) const tabLayouts = useRef<Map<string, {x: number; width: number}>>(new Map())
@@ -14,7 +14,7 @@ export function UserSearchInput({
value: string value: string
onChangeText: (text: string) => void onChangeText: (text: string) => void
onEscape: () => void onEscape: () => void
inputRef: React.RefObject<TextInput | null> inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null>
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
+3 -3
View File
@@ -11,7 +11,7 @@ import {normalizeTextStyles} from '#/alf/typography'
import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env'
export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & { export type AutosizedTextareaProps = Omit<TextInputProps, 'multiline'> & {
ref?: React.Ref<TextInput> ref?: React.Ref<React.ComponentRef<typeof TextInput>>
label: string label: string
minRows?: number minRows?: number
maxRows?: number maxRows?: number
@@ -43,7 +43,7 @@ export function AutosizedTextarea({
...rest ...rest
}: AutosizedTextareaProps) { }: AutosizedTextareaProps) {
const {theme: t, fonts} = useAlf() const {theme: t, fonts} = useAlf()
const internalRef = useRef<TextInput>(null) const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const {style, minInputHeight, maxInputHeight, verticalContentPadding} = const {style, minInputHeight, maxInputHeight, verticalContentPadding} =
useMemo(() => { useMemo(() => {
const normalizedStyles = normalizeTextStyles( const normalizedStyles = normalizeTextStyles(
@@ -180,7 +180,7 @@ export function AutosizedTextarea({
]} ]}
{...rest} {...rest}
ref={mergeRefs([ ref={mergeRefs([
(node: TextInput | null) => { (node: React.ComponentRef<typeof TextInput> | null) => {
internalRef.current = node internalRef.current = node
// bop resize on first render // bop resize on first render
if (IS_WEB && node) handleResizeWeb() if (IS_WEB && node) handleResizeWeb()
+1 -1
View File
@@ -62,7 +62,7 @@ export function DateField({
<TextField.Icon icon={CalendarDays} /> <TextField.Icon icon={CalendarDays} />
<Input <Input
value={value === '' ? '' : toSimpleDateString(value)} value={value === '' ? '' : toSimpleDateString(value)}
inputRef={inputRef as React.Ref<TextInput>} inputRef={inputRef as React.Ref<React.ComponentRef<typeof TextInput>>}
label={label} label={label}
onChange={handleOnChange} onChange={handleOnChange}
testID={testID} testID={testID}
+2 -2
View File
@@ -19,7 +19,7 @@ type Props = Omit<TextField.InputProps, 'label'> & {
*/ */
onClearText?: () => void onClearText?: () => void
hotkey?: boolean hotkey?: boolean
ref?: React.Ref<TextInput> ref?: React.Ref<React.ComponentRef<typeof TextInput>>
} }
export function SearchInput({ export function SearchInput({
@@ -33,7 +33,7 @@ export function SearchInput({
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const showClear = value && value.length > 0 const showClear = value && value.length > 0
const internalRef = useRef<TextInput>(null) const internalRef = useRef<React.ComponentRef<typeof TextInput>>(null)
useEffect(() => { useEffect(() => {
if (!hotkey) return if (!hotkey) return
+37 -31
View File
@@ -14,6 +14,8 @@ import {mergeRefs} from '#/lib/merge-refs'
import { import {
applyFonts, applyFonts,
atoms as a, atoms as a,
flatten,
type MutableTextStyle,
platform, platform,
type TextStyleProp, type TextStyleProp,
tokens, tokens,
@@ -26,7 +28,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const Context = createContext<{ const Context = createContext<{
inputRef: React.RefObject<TextInput | null> | null inputRef: React.RefObject<React.ComponentRef<typeof TextInput> | null> | null
isInvalid: boolean isInvalid: boolean
hovered: boolean hovered: boolean
onHoverIn: () => void onHoverIn: () => void
@@ -55,7 +57,7 @@ export type RootProps = React.PropsWithChildren<
> >
export function Root({children, isInvalid = false, style}: RootProps) { export function Root({children, isInvalid = false, style}: RootProps) {
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const { const {
state: hovered, state: hovered,
onIn: onHoverIn, onIn: onHoverIn,
@@ -161,7 +163,9 @@ export type InputProps = Omit<
value?: string value?: string
onChangeText?: (value: string) => void onChangeText?: (value: string) => void
isInvalid?: boolean isInvalid?: boolean
inputRef?: React.RefObject<TextInput | null> | React.ForwardedRef<TextInput> inputRef?:
| React.RefObject<React.ComponentRef<typeof TextInput> | null>
| React.ForwardedRef<React.ComponentRef<typeof TextInput>>
/** /**
* Note: this currently falls back to the label if not specified. However, * Note: this currently falls back to the label if not specified. However,
* most new designs have no placeholder. We should eventually remove this fallback * most new designs have no placeholder. We should eventually remove this fallback
@@ -208,34 +212,36 @@ export function createInput(Component: typeof TextInput) {
const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean)) const refs = mergeRefs([ctx.inputRef, inputRef!].filter(Boolean))
const flattened = StyleSheet.flatten<TextStyle>([ const flattened: MutableTextStyle = {
a.relative, ...flatten([
a.z_20, a.relative,
a.flex_1, a.z_20,
a.text_md, a.flex_1,
t.atoms.text, a.text_md,
a.px_xs, t.atoms.text,
{ a.px_xs,
// paddingVertical doesn't work w/multiline - esb {
lineHeight: a.text_md.fontSize * 1.2, // paddingVertical doesn't work w/multiline - esb
textAlignVertical: rest.multiline ? 'top' : undefined, lineHeight: a.text_md.fontSize * 1.2,
minHeight: rest.multiline ? 80 : undefined, textAlignVertical: rest.multiline ? 'top' : undefined,
minWidth: 0, minHeight: rest.multiline ? 80 : undefined,
paddingTop: 13, minWidth: 0,
paddingBottom: 13, paddingTop: 13,
}, paddingBottom: 13,
/* },
* Margins are needed here to avoid autofill background overlapping the /*
* top and bottom borders - esb * Margins are needed here to avoid autofill background overlapping the
*/ * top and bottom borders - esb
web({ */
paddingTop: 11, web({
paddingBottom: 11, paddingTop: 11,
marginTop: 2, paddingBottom: 11,
marginBottom: 2, marginTop: 2,
}), marginBottom: 2,
style, }),
]) style,
]),
}
applyFonts(flattened, fonts.family) applyFonts(flattened, fonts.family)
+2 -2
View File
@@ -1,13 +1,13 @@
import {useEffect} from 'react' import {useEffect} from 'react'
import { import {
Keyboard, Keyboard,
type KeyboardEventListener, type KeyboardEvent,
type KeyboardEventName, type KeyboardEventName,
} from 'react-native' } from 'react-native'
export function useOnKeyboard( export function useOnKeyboard(
eventName: KeyboardEventName, eventName: KeyboardEventName,
cb: KeyboardEventListener, cb: (event: KeyboardEvent) => unknown,
) { ) {
useEffect(() => { useEffect(() => {
const subscription = Keyboard.addListener(eventName, cb) const subscription = Keyboard.addListener(eventName, cb)
+7 -5
View File
@@ -65,7 +65,7 @@ interface GalleryProps {
} }
const Context = createContext<{ const Context = createContext<{
bleedRef: React.RefObject<View | null> bleedRef: React.RefObject<React.ComponentRef<typeof View> | null>
bleedWidth: number bleedWidth: number
}>({ }>({
bleedRef: {current: null}, bleedRef: {current: null},
@@ -73,7 +73,7 @@ const Context = createContext<{
}) })
export function GalleryBleed({children}: {children: React.ReactNode}) { export function GalleryBleed({children}: {children: React.ReactNode}) {
const ref = useRef<View>(null) const ref = useRef<React.ComponentRef<typeof View>>(null)
const [bleedWidth, setBleedWidth] = useState(0) const [bleedWidth, setBleedWidth] = useState(0)
if (!isValidElement(children)) { if (!isValidElement(children)) {
@@ -145,7 +145,7 @@ export function Gallery({
* scroll position, so it works correctly for off-screen FlatList items. * scroll position, so it works correctly for off-screen FlatList items.
*/ */
const {bleedRef, bleedWidth} = useGalleryBleed() const {bleedRef, bleedWidth} = useGalleryBleed()
const contentRef = useRef<View>(null) const contentRef = useRef<React.ComponentRef<typeof View>>(null)
const [contentDims, setContentDims] = useState<{x: number; width: number}>() const [contentDims, setContentDims] = useState<{x: number; width: number}>()
const measure = () => { const measure = () => {
if (contentRef.current && bleedRef.current) { if (contentRef.current && bleedRef.current) {
@@ -168,7 +168,9 @@ export function Gallery({
const flatListRef = useRef<FlatList>(null) const flatListRef = useRef<FlatList>(null)
const itemWidthsRef = useRef<Map<number, number>>(new Map()) const itemWidthsRef = useRef<Map<number, number>>(new Map())
const itemRefsRef = useRef<Map<number, View>>(new Map()) const itemRefsRef = useRef<Map<number, React.ComponentRef<typeof View>>>(
new Map(),
)
const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map()) const containerRefsRef = useRef<Map<number, AnimatedRef<any>>>(new Map())
const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map()) const thumbDimsRef = useRef<Map<number, Dimensions>>(new Map())
const currentIndexRef = useRef(0) const currentIndexRef = useRef(0)
@@ -408,7 +410,7 @@ function GalleryImage({
index: number index: number
imageCount: number imageCount: number
onWidthChange: (index: number, width: number) => void onWidthChange: (index: number, width: number) => void
itemRef: (node: View | null) => void itemRef: (node: React.ComponentRef<typeof View> | null) => void
largeAltBadge?: boolean largeAltBadge?: boolean
onContainerRef: (index: number, ref: AnimatedRef<any>) => void onContainerRef: (index: number, ref: AnimatedRef<any>) => void
onThumbDims: (index: number, dims: Dimensions) => void onThumbDims: (index: number, dims: Dimensions) => void
+1 -1
View File
@@ -226,7 +226,7 @@ function BlockDialogInner({
<View style={[a.py_lg, a.align_center, a.justify_center]}> <View style={[a.py_lg, a.align_center, a.justify_center]}>
<Loader size="lg" /> <Loader size="lg" />
</View> </View>
) : null ) : undefined
} }
footer={ footer={
<Dialog.FlatListFooter <Dialog.FlatListFooter
@@ -159,7 +159,7 @@ function Inner(
const logger = ax.logger.useChild(ax.logger.Context.ReportDialog) const logger = ax.logger.useChild(ax.logger.Context.ReportDialog)
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const ref = useRef<ScrollView>(null) const ref = useRef<React.ComponentRef<typeof ScrollView>>(null)
const { const {
data: allLabelers, data: allLabelers,
isLoading: isLabelerLoading, isLoading: isLabelerLoading,
+1 -1
View File
@@ -60,7 +60,7 @@ function GifPickerBody({
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void onSelectGif: (gif: Gif) => void
}) { }) {
const textInputRef = useRef<TextInput>(null) const textInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('') const [rawSearch, setRawSearch] = useState('')
const [activeCategory, setActiveCategory] = useState<string>('trending') const [activeCategory, setActiveCategory] = useState<string>('trending')
@@ -89,7 +89,7 @@ export const GifPickerGrid = forwardRef<ListMethods, Props>(
onRetry={fetchNextPage} onRetry={fetchNextPage}
style={{borderTopWidth: 0}} style={{borderTopWidth: 0}}
/> />
) : null ) : undefined
} }
/> />
) )
@@ -15,7 +15,7 @@ export function GifPickerHeader({
onEscape, onEscape,
canClear, canClear,
}: { }: {
inputRef: Ref<TextInput> inputRef: Ref<React.ComponentRef<typeof TextInput>>
onChangeText: (text: string) => void onChangeText: (text: string) => void
onClear: () => void onClear: () => void
onEscape: () => void onEscape: () => void
+5 -1
View File
@@ -1 +1,5 @@
export {unstable_batchedUpdates as batchedUpdates} from 'react-native' import {unstable_batchedUpdates} from 'react-native'
export function batchedUpdates(fn: () => void): void {
unstable_batchedUpdates(fn, undefined)
}
+7 -7
View File
@@ -1,18 +1,18 @@
import {type ForwardedRef, useEffect, useMemo, useRef} from 'react' import {type ForwardedRef, useEffect, useMemo, useRef} from 'react'
import {type ScrollView} from 'react-native' import {Platform, type ScrollView} from 'react-native'
import {Platform} from 'react-native'
import {mergeRefs} from '#/lib/merge-refs' import {mergeRefs} from '#/lib/merge-refs'
type Props<Scrollable extends ScrollView = ScrollView> = { type ScrollViewInstance = React.ComponentRef<typeof ScrollView>
type Props<Scrollable extends ScrollViewInstance = ScrollViewInstance> = {
cursor?: string cursor?: string
outerRef?: ForwardedRef<Scrollable> outerRef?: ForwardedRef<Scrollable>
} }
export function useDraggableScroll<Scrollable extends ScrollView = ScrollView>({ export function useDraggableScroll<
outerRef, Scrollable extends ScrollViewInstance = ScrollViewInstance,
cursor = 'grab', >({outerRef, cursor = 'grab'}: Props<Scrollable> = {}) {
}: Props<Scrollable> = {}) {
const ref = useRef<Scrollable>(null) const ref = useRef<Scrollable>(null)
useEffect(() => { useEffect(() => {
+3 -3
View File
@@ -221,7 +221,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
) )
const buttons = jest.mocked(Alert.alert).mock.calls[0][2] const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
act(() => buttons?.[1].onPress?.()) void act(() => buttons?.[1].onPress?.())
await waitFor(() => expect(reloadAsync).toHaveBeenCalled()) await waitFor(() => expect(reloadAsync).toHaveBeenCalled())
expect(device.set).toHaveBeenCalledWith(['pendingOTAUpdate'], { expect(device.set).toHaveBeenCalledWith(['pendingOTAUpdate'], {
@@ -256,7 +256,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
await act(() => result.current.tryApplyUpdate('pull-request-123')) await act(() => result.current.tryApplyUpdate('pull-request-123'))
const buttons = jest.mocked(Alert.alert).mock.calls[0][2] const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
act(() => buttons?.[1].onPress?.()) void act(() => buttons?.[1].onPress?.())
await waitFor(() => await waitFor(() =>
expect(Alert.alert).toHaveBeenLastCalledWith( expect(Alert.alert).toHaveBeenLastCalledWith(
@@ -283,7 +283,7 @@ describe('useApplyPullRequestOTAUpdate', () => {
await act(() => result.current.tryApplyUpdate('pull-request-123')) await act(() => result.current.tryApplyUpdate('pull-request-123'))
const buttons = jest.mocked(Alert.alert).mock.calls[0][2] const buttons = jest.mocked(Alert.alert).mock.calls[0][2]
act(() => buttons?.[1].onPress?.()) void act(() => buttons?.[1].onPress?.())
await waitFor(() => await waitFor(() =>
expect(Alert.alert).toHaveBeenLastCalledWith( expect(Alert.alert).toHaveBeenLastCalledWith(
+1 -1
View File
@@ -452,7 +452,7 @@ export const splash = (scheme: 'light' | 'dark') => {
: require('../../../assets/splash/splash-dark.png') : require('../../../assets/splash/splash-dark.png')
return { return {
image: RNImage.resolveAssetSource(source).uri, image: RNImage.resolveAssetSource(source)!.uri,
imageFullScreen: true, imageFullScreen: true,
imageResizeMode: 'cover', imageResizeMode: 'cover',
backgroundColor: scheme === 'light' ? '#006AFF' : '#002861', backgroundColor: scheme === 'light' ? '#006AFF' : '#002861',
+2
View File
@@ -23,6 +23,7 @@ export async function openPicker(opts?: ImagePickerOptions) {
quality: 1, quality: 1,
selectionLimit: 1, selectionLimit: 1,
...opts, ...opts,
shouldDownloadFromNetwork: true,
legacy: true, legacy: true,
preferredAssetRepresentationMode: preferredAssetRepresentationMode:
UIImagePickerPreferredAssetRepresentationMode.Automatic, UIImagePickerPreferredAssetRepresentationMode.Automatic,
@@ -63,6 +64,7 @@ export async function openUnifiedPicker({
// the `file` returned on each asset after validation instead. // the `file` returned on each asset after validation instead.
base64: false, base64: false,
selectionLimit: IS_IOS ? selectionCountRemaining : undefined, selectionLimit: IS_IOS ? selectionCountRemaining : undefined,
shouldDownloadFromNetwork: true,
preferredAssetRepresentationMode: preferredAssetRepresentationMode:
UIImagePickerPreferredAssetRepresentationMode.Automatic, UIImagePickerPreferredAssetRepresentationMode.Automatic,
videoExportPreset: VideoExportPreset.Passthrough, videoExportPreset: VideoExportPreset.Passthrough,
+2 -2
View File
@@ -78,8 +78,8 @@ export const LoginForm = ({
const [identifier, setIdentifier] = useState(initialHandle || '') const [identifier, setIdentifier] = useState(initialHandle || '')
const [identifierFocused, setIdentifierFocused] = useState(false) const [identifierFocused, setIdentifierFocused] = useState(false)
const [authFactorToken, setAuthFactorToken] = useState('') const [authFactorToken, setAuthFactorToken] = useState('')
const identifierRef = useRef<TextInput>(null) const identifierRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const passwordRef = useRef<TextInput>(null) const passwordRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const hasFocusedOnce = useRef(false) const hasFocusedOnce = useRef(false)
const [hasPassword, setHasPassword] = useState(false) const [hasPassword, setHasPassword] = useState(false)
const [revealPassword, setRevealPassword] = useState(false) const [revealPassword, setRevealPassword] = useState(false)
+1 -1
View File
@@ -379,7 +379,7 @@ function JoinRequestsList({
style={[a.flex_1, a.align_center, a.justify_center, a.py_4xl]}> style={[a.flex_1, a.align_center, a.justify_center, a.py_4xl]}>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>
) : null ) : undefined
} }
contentContainerStyle={ contentContainerStyle={
showFooter ? {paddingBottom: footerHeight} : undefined showFooter ? {paddingBottom: footerHeight} : undefined
@@ -189,7 +189,7 @@ export function MessageComposer({
<View <View
collapsable={false} collapsable={false}
ref={native( ref={native(
(node: View) => (node: React.ComponentRef<typeof View>) =>
void composerInternalApiRef.current?.setAutocompleteAnchor(node), void composerInternalApiRef.current?.setAutocompleteAnchor(node),
)}> )}>
<GlassContainer <GlassContainer
@@ -209,7 +209,11 @@ export function MessageComposer({
composerInternalApiRef.current?.insert(emoji.native) composerInternalApiRef.current?.insert(emoji.native)
} }
nextFocusRef={() => nextFocusRef={() =>
composerInternalApiRef.current?.input?.element composerInternalApiRef.current?.input
?.element as unknown as
| {focus: () => void}
| null
| undefined
}> }>
<EmojiPicker.Trigger label={l`Open emoji picker`}> <EmojiPicker.Trigger label={l`Open emoji picker`}>
{({props, state, control}) => ( {({props, state, control}) => (
+1 -1
View File
@@ -35,7 +35,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const onboardDispatch = useOnboardingDispatch() const onboardDispatch = useOnboardingDispatch()
const {state, dispatch} = useOnboardingInternalState() const {state, dispatch} = useOnboardingInternalState()
const scrollview = useRef<ScrollView>(null) const scrollview = useRef<React.ComponentRef<typeof ScrollView>>(null)
const prevActiveStep = useRef<string>(state.activeStep) const prevActiveStep = useRef<string>(state.activeStep)
useEffect(() => { useEffect(() => {
@@ -1,6 +1,7 @@
import {useRef, useState} from 'react' import {useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import PagerView from 'react-native-pager-view' import PagerView from 'react-native-pager-view'
import {type PagerViewOnPageSelectedEvent} from 'react-native-pager-view'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -36,7 +37,7 @@ export function ValuePropositionPager({
ref={ref} ref={ref}
style={[a.flex_1]} style={[a.flex_1]}
initialPage={step} initialPage={step}
onPageSelected={evt => { onPageSelected={(evt: PagerViewOnPageSelectedEvent) => {
const page = evt.nativeEvent.position as 0 | 1 | 2 const page = evt.nativeEvent.position as 0 | 1 | 2
if (step !== page) { if (step !== page) {
setActivePage(page) setActivePage(page)
@@ -378,7 +378,7 @@ function SuggestedProfileCard({
}) { }) {
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const cardRef = useRef<View>(null) const cardRef = useRef<React.ComponentRef<typeof View>>(null)
const hasTrackedRef = useRef(false) const hasTrackedRef = useRef(false)
useEffect(() => { useEffect(() => {
+2 -2
View File
@@ -160,8 +160,8 @@ export function PostThread({uri}: {uri: string}) {
const totalParentCount = useRef(0) // recomputed below const totalParentCount = useRef(0) // recomputed below
const totalChildrenCount = useRef(thread.data.items.length) // recomputed below const totalChildrenCount = useRef(thread.data.items.length) // recomputed below
const listRef = useRef<ListMethods>(null) const listRef = useRef<ListMethods>(null)
const anchorRef = useRef<View | null>(null) const anchorRef = useRef<React.ComponentRef<typeof View> | null>(null)
const headerRef = useRef<View | null>(null) const headerRef = useRef<React.ComponentRef<typeof View> | null>(null)
/* /*
* On a cold load, parents are not prepended until the anchor post has * On a cold load, parents are not prepended until the anchor post has
+2 -1
View File
@@ -1,6 +1,7 @@
import {useCallback, useMemo, useRef, useState} from 'react' import {useCallback, useMemo, useRef, useState} from 'react'
import {View, type ViewabilityConfig} from 'react-native' import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {type ViewabilityConfig} from '@react-native/virtualized-lists'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import * as bcp47Match from 'bcp-47-match' import * as bcp47Match from 'bcp-47-match'
+1 -1
View File
@@ -115,7 +115,7 @@ export function SearchScreenShell({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const route = useRoute() const route = useRoute()
const textInput = useRef<TextInput>(null) const textInput = useRef<React.ComponentRef<typeof TextInput>>(null)
const {t: l} = useLingui() const {t: l} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -28,7 +28,7 @@ export function ClearableInput({
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const [showClear, setShowClear] = useState(defaultValue.length > 0) const [showClear, setShowClear] = useState(defaultValue.length > 0)
return ( return (
@@ -137,8 +137,8 @@ function DialogInner({
const [dateUntilActive, setDateUntilActive] = useState(!!parsed.until) const [dateUntilActive, setDateUntilActive] = useState(!!parsed.until)
const [filters, setFilters] = useState<AdvancedFilter[]>(parsed.filters) const [filters, setFilters] = useState<AdvancedFilter[]>(parsed.filters)
const scrollRef = useRef<ScrollView>(null) const scrollRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const filtersSectionRef = useRef<View>(null) const filtersSectionRef = useRef<React.ComponentRef<typeof View>>(null)
function addFilter() { function addFilter() {
if (filters.length >= MAX_FILTERS) return if (filters.length >= MAX_FILTERS) return
@@ -37,7 +37,7 @@ export function SearchAutocompleteInput({
* keystroke while the input is still focused. * keystroke while the input is still focused.
*/ */
const [dismissed, setDismissed] = useState(false) const [dismissed, setDismissed] = useState(false)
const inputRef = useRef<TextInput>(null) const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const sift = useSift({ const sift = useSift({
offset: a.p_sm.padding, offset: a.p_sm.padding,
@@ -1,9 +1,9 @@
import {type ImageSourcePropType} from 'react-native' import {type ImageSource} from 'expo-image'
import type * as DynamicAppIcon from '@bsky.app/expo-dynamic-app-icon' import type * as DynamicAppIcon from '@bsky.app/expo-dynamic-app-icon'
export type AppIconSet = { export type AppIconSet = {
id: DynamicAppIcon.IconName id: DynamicAppIcon.IconName
name: string name: string
iosImage: () => ImageSourcePropType iosImage: () => ImageSource
androidImage: () => ImageSourcePropType androidImage: () => ImageSource
} }
@@ -128,7 +128,7 @@ export function ActivityNotificationSettingsScreen({}: Props) {
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={4} onEndReachedThreshold={4}
ListEmptyComponent={ ListEmptyComponent={
error ? null : ( error ? undefined : (
<View style={[a.px_xl, a.py_md]}> <View style={[a.px_xl, a.py_md]}>
{!isPending ? ( {!isPending ? (
<Admonition.Outer type="tip"> <Admonition.Outer type="tip">
@@ -73,7 +73,7 @@ function DeleteAccountDialogInner({
control: DialogOuterProps['control'] control: DialogOuterProps['control']
deactivateDialogControl: DialogOuterProps['control'] deactivateDialogControl: DialogOuterProps['control']
}) { }) {
const passwordRef = useRef<TextInput | null>(null) const passwordRef = useRef<React.ComponentRef<typeof TextInput> | null>(null)
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const cleanError = useCleanError() const cleanError = useCleanError()
+2 -2
View File
@@ -68,8 +68,8 @@ export function StepInfo({
const prevEmailValueRef = useRef<string>(state.email) const prevEmailValueRef = useRef<string>(state.email)
const passwordValueRef = useRef<string>(state.password) const passwordValueRef = useRef<string>(state.password)
const emailInputRef = useRef<TextInput>(null) const emailInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const passwordInputRef = useRef<TextInput>(null) const passwordInputRef = useRef<React.ComponentRef<typeof TextInput>>(null)
const birthdateInputRef = useRef<DateFieldRef>(null) const birthdateInputRef = useRef<DateFieldRef>(null)
const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback() const aaRegionConfig = useAgeAssuranceRegionConfigWithFallback()
@@ -41,6 +41,7 @@ import {
} from '#/state/shell/progress-guide' } from '#/state/shell/progress-guide'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader' import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
import {type ListRef} from '#/view/com/util/List'
import {bulkWriteFollows} from '#/screens/Onboarding/util' import {bulkWriteFollows} from '#/screens/Onboarding/util'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -251,8 +252,7 @@ function StarterPackScreenLoaded({
// Validated above // Validated above
listUri={starterPack.list!.uri} listUri={starterPack.list!.uri}
headerHeight={headerHeight} headerHeight={headerHeight}
// @ts-expect-error scrollElRef={scrollElRef as ListRef}
scrollElRef={scrollElRef}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
/> />
) )
@@ -260,11 +260,9 @@ function StarterPackScreenLoaded({
{showFeedsTab {showFeedsTab
? ({headerHeight, scrollElRef}) => ( ? ({headerHeight, scrollElRef}) => (
<FeedsList <FeedsList
// @ts-expect-error ? feeds={starterPack.feeds!}
feeds={starterPack?.feeds}
headerHeight={headerHeight} headerHeight={headerHeight}
// @ts-expect-error scrollElRef={scrollElRef as ListRef}
scrollElRef={scrollElRef}
/> />
) )
: null} : null}
@@ -274,9 +272,7 @@ function StarterPackScreenLoaded({
// Validated above // Validated above
listUri={starterPack.list!.uri} listUri={starterPack.list!.uri}
headerHeight={headerHeight} headerHeight={headerHeight}
// @ts-expect-error scrollElRef={scrollElRef as ListRef}
scrollElRef={scrollElRef}
moderationOpts={moderationOpts}
/> />
) )
: null} : null}
+5 -3
View File
@@ -5,8 +5,6 @@ import {
Pressable, Pressable,
ScrollView, ScrollView,
View, View,
type ViewabilityConfig,
type ViewToken,
} from 'react-native' } from 'react-native'
import { import {
Gesture, Gesture,
@@ -29,6 +27,10 @@ import {AtUri} from '@atproto/syntax'
import {type ModerationDecision} from '@bsky/sdk/moderation' import {type ModerationDecision} from '@bsky/sdk/moderation'
import {RichText as RichTextAPI} from '@bsky/sdk/richtext' import {RichText as RichTextAPI} from '@bsky/sdk/richtext'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {
type ListViewToken as ViewToken,
type ViewabilityConfig,
} from '@react-native/virtualized-lists'
import { import {
type RouteProp, type RouteProp,
useFocusEffect, useFocusEffect,
@@ -413,7 +415,7 @@ function Feed() {
const onViewableItemsChanged = useCallback( const onViewableItemsChanged = useCallback(
({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
if (viewableItems[0] && viewableItems[0].index !== null) { if (viewableItems[0]?.index != null) {
const newIndex = viewableItems[0].index const newIndex = viewableItems[0].index
setCurrentIndex(newIndex) setCurrentIndex(newIndex)
updateVideoState(newIndex) updateVideoState(newIndex)
+2 -2
View File
@@ -15,10 +15,10 @@ import {Button, ButtonText} from '#/components/Button'
import splashImagePointer from '../../../../assets/illustrations/illustration-mobile.png' import splashImagePointer from '../../../../assets/illustrations/illustration-mobile.png'
// @ts-ignore // @ts-ignore
import darkSplashImagePointer from '../../../../assets/illustrations/illustration-mobile-dark.png' import darkSplashImagePointer from '../../../../assets/illustrations/illustration-mobile-dark.png'
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri const splashImageUri = RNImage.resolveAssetSource(splashImagePointer)!.uri
const darkSplashImageUri = RNImage.resolveAssetSource( const darkSplashImageUri = RNImage.resolveAssetSource(
darkSplashImagePointer, darkSplashImagePointer,
).uri )!.uri
export const SplashScreen = ({ export const SplashScreen = ({
onPressSignin, onPressSignin,
-1
View File
@@ -73,7 +73,6 @@ export const SplashScreen = ({
style={[ style={[
a.h_full, a.h_full,
a.justify_center, a.justify_center,
// @ts-expect-error web only
{paddingBottom: '20vh'}, {paddingBottom: '20vh'},
IS_WEB_MOBILE && a.pb_5xl, IS_WEB_MOBILE && a.pb_5xl,
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
+2 -1
View File
@@ -1,5 +1,5 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {Pressable, View} from 'react-native' import {View} from 'react-native'
import * as VideoThumbnails from 'expo-video-thumbnails' import * as VideoThumbnails from 'expo-video-thumbnails'
import {msg, plural} from '@lingui/core/macro' import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -15,6 +15,7 @@ import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsIcon} from '#/components/icons
import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote' import {CloseQuote_Stroke2_Corner0_Rounded as CloseQuoteIcon} from '#/components/icons/Quote'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
import * as MediaPreview from '#/components/MediaPreview' import * as MediaPreview from '#/components/MediaPreview'
import {Pressable} from '#/components/Pressable'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
+1 -1
View File
@@ -162,7 +162,7 @@ const GalleryItem = ({
const editControl = Dialog.useDialogControl() const editControl = Dialog.useDialogControl()
const [altBtnViewTag, setAltBtnViewTag] = useState<number>() const [altBtnViewTag, setAltBtnViewTag] = useState<number>()
const altBtnRef = (node: View | null) => { const altBtnRef = (node: React.ComponentRef<typeof View> | null) => {
// for iOS 26 fluid transition // for iOS 26 fluid transition
if (IS_IOS && node) { if (IS_IOS && node) {
const tag = findNodeHandle(node) const tag = findNodeHandle(node)
@@ -50,7 +50,7 @@ export function TextInput({
}: TextInputProps) { }: TextInputProps) {
const {t: l} = useLingui() const {t: l} = useLingui()
const {theme: t, fonts} = useAlf() const {theme: t, fonts} = useAlf()
const textInput = useRef<RNTextInput>(null) const textInput = useRef<React.ComponentRef<typeof RNTextInput>>(null)
const textInputSelection = useRef<Selection>({start: 0, end: 0}) const textInputSelection = useRef<Selection>({start: 0, end: 0})
const theme = useTheme() const theme = useTheme()
const [autocompletePrefix, setAutocompletePrefix] = useState('') const [autocompletePrefix, setAutocompletePrefix] = useState('')
@@ -178,9 +178,10 @@ export function TextInput({
*/ */
if (IS_ANDROID) { if (IS_ANDROID) {
// @ts-ignore // @ts-ignore
style.fontVariant = style.fontVariant style.fontVariant =
? style.fontVariant.join(' ') typeof style.fontVariant === 'string'
: undefined ? style.fontVariant
: style.fontVariant?.join(' ')
} }
return style return style
}, [t, fonts]) }, [t, fonts])
@@ -1,6 +1,6 @@
import {type QueryClient} from '@tanstack/react-query' import {type QueryClient} from '@tanstack/react-query'
import {atoms as a, flatten} from '#/alf' import {atoms as a, flattenToCSS} from '#/alf'
export function clearThumbnailCache(_queryClient: QueryClient) { export function clearThumbnailCache(_queryClient: QueryClient) {
// no-op on web // no-op on web
@@ -10,7 +10,7 @@ export function VideoTranscodeBackdrop({uri}: {uri: string}) {
return ( return (
<video <video
src={uri} src={uri}
style={flatten([ style={flattenToCSS([
a.absolute, a.absolute,
a.inset_0, a.inset_0,
a.h_full, a.h_full,
-1
View File
@@ -145,7 +145,6 @@ export function ComposerPrompt() {
return ( return (
<Pressable <Pressable
onPress={onPress} onPress={onPress}
android_ripple={null}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Compose new post`)} accessibilityLabel={_(msg`Compose new post`)}
accessibilityHint={_(msg`Opens the post composer`)} accessibilityHint={_(msg`Opens the post composer`)}
+1 -1
View File
@@ -9,7 +9,7 @@ export function DraggableScrollView({
style, style,
...props ...props
}: ComponentPropsWithRef<typeof ScrollView>) { }: ComponentPropsWithRef<typeof ScrollView>) {
const {refs} = useDraggableScroll<ScrollView>({ const {refs} = useDraggableScroll<React.ComponentRef<typeof ScrollView>>({
outerRef: ref, outerRef: ref,
cursor: 'grab', // optional, default cursor: 'grab', // optional, default
}) })
+5 -3
View File
@@ -31,7 +31,9 @@ import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams { export interface PagerWithHeaderChildParams {
headerHeight: number headerHeight: number
isFocused: boolean isFocused: boolean
scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> scrollElRef: React.MutableRefObject<
ListMethods | React.ComponentRef<typeof ScrollView> | null
>
} }
export interface PagerWithHeaderProps { export interface PagerWithHeaderProps {
@@ -269,7 +271,7 @@ let PagerTabBar = ({
], ],
} }
}) })
const headerRef = useRef<View>(null) const headerRef = useRef<React.ComponentRef<typeof View>>(null)
const fallbackHeaderOnlyHeight = useRef(0) const fallbackHeaderOnlyHeight = useRef(0)
return ( return (
<Animated.View <Animated.View
@@ -378,7 +380,7 @@ function PagerItem({
headerHeight, headerHeight,
isFocused, isFocused,
scrollElRef: scrollElRef as React.MutableRefObject< scrollElRef: scrollElRef as React.MutableRefObject<
ListMethods | ScrollView | null ListMethods | React.ComponentRef<typeof ScrollView> | null
>, >,
})} })}
</ScrollProvider> </ScrollProvider>
+4 -2
View File
@@ -16,7 +16,9 @@ import {TabBar} from './TabBar'
export interface PagerWithHeaderChildParams { export interface PagerWithHeaderChildParams {
headerHeight: number headerHeight: number
isFocused: boolean isFocused: boolean
scrollElRef: React.MutableRefObject<ListMethods | ScrollView | null> scrollElRef: React.MutableRefObject<
ListMethods | React.ComponentRef<typeof ScrollView> | null
>
} }
export interface PagerWithHeaderProps { export interface PagerWithHeaderProps {
@@ -180,7 +182,7 @@ function PagerItem({
headerHeight: 0, headerHeight: 0,
isFocused, isFocused,
scrollElRef: scrollElRef as React.MutableRefObject< scrollElRef: scrollElRef as React.MutableRefObject<
ListMethods | ScrollView | null ListMethods | React.ComponentRef<typeof ScrollView> | null
>, >,
}) })
} }
+4 -2
View File
@@ -38,7 +38,7 @@ export function TabBar({
onPressSelected, onPressSelected,
}: TabBarProps) { }: TabBarProps) {
const t = useTheme() const t = useTheme()
const scrollElRef = useRef<ScrollView>(null) const scrollElRef = useRef<React.ComponentRef<typeof ScrollView>>(null)
const itemRefs = useRef<Array<Element>>([]) const itemRefs = useRef<Array<Element>>([])
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const styles = gtMobile ? desktopStyles : mobileStyles const styles = gtMobile ? desktopStyles : mobileStyles
@@ -47,7 +47,9 @@ export function TabBar({
// On the web, the primary interaction is tapping. // On the web, the primary interaction is tapping.
// Scrolling under tap feels disorienting so only adjust the scroll offset // Scrolling under tap feels disorienting so only adjust the scroll offset
// when tapping on an item out of view--and we adjust by almost an entire page. // when tapping on an item out of view--and we adjust by almost an entire page.
const parent = scrollElRef?.current?.getScrollableNode?.() const parent = scrollElRef.current?.getScrollableNode?.() as unknown as
| HTMLElement
| undefined
if (!parent) { if (!parent) {
return return
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import {type AlertButton, type AlertStatic} from 'react-native' import {type Alert as RNAlert, type AlertButton} from 'react-native'
class WebAlert implements Pick<AlertStatic, 'alert'> { class WebAlert implements Pick<typeof RNAlert, 'alert'> {
public alert(title: string, message?: string, buttons?: AlertButton[]): void { public alert(title: string, message?: string, buttons?: AlertButton[]): void {
if (buttons === undefined || buttons.length === 0) { if (buttons === undefined || buttons.length === 0) {
window.alert([title, message].filter(Boolean).join('\n')) window.alert([title, message].filter(Boolean).join('\n'))
+1 -2
View File
@@ -1,5 +1,5 @@
import {forwardRef, memo, useDeferredValue, useMemo} from 'react' import {forwardRef, memo, useDeferredValue, useMemo} from 'react'
import {RefreshControl, type ViewToken} from 'react-native' import {type ListViewToken as ViewToken, RefreshControl} from 'react-native'
import { import {
type FlatListPropsWithLayout, type FlatListPropsWithLayout,
useAnimatedScrollHandler, useAnimatedScrollHandler,
@@ -173,7 +173,6 @@ let List = forwardRef<ListMethods, ListProps>(
scrollsToTop={scrollsToTop} scrollsToTop={scrollsToTop}
scrollEventThrottle={1} scrollEventThrottle={1}
style={style} style={style}
// @ts-expect-error FlatList_INTERNAL ref type is wrong -sfn
ref={ref} ref={ref}
/> />
) )
+1 -1
View File
@@ -407,7 +407,7 @@ function ListImpl<ItemT>(
overflowY: isWithinSplitView ? 'auto' : 'scroll', overflowY: isWithinSplitView ? 'auto' : 'scroll',
}, },
]} ]}
ref={nativeRef as unknown as React.RefObject<View>}> ref={nativeRef as unknown as React.Ref<React.ComponentRef<typeof View>>}>
<Visibility <Visibility
onVisibleChange={setIsInsideVisibleTree} onVisibleChange={setIsInsideVisibleTree}
style={ style={
+9 -16
View File
@@ -1,26 +1,19 @@
import {forwardRef, type PropsWithChildren} from 'react' import {Pressable, type StyleProp, type ViewStyle} from 'react-native'
import {
Pressable,
type PressableProps,
type StyleProp,
type View,
type ViewStyle,
} from 'react-native'
import {addStyle} from '#/lib/styles' import {addStyle} from '#/lib/styles'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
interface PressableWithHover extends PressableProps { interface PressableWithHoverProps extends React.ComponentPropsWithRef<Pressable> {
hoverStyle: StyleProp<ViewStyle> hoverStyle: StyleProp<ViewStyle>
} }
export const PressableWithHover = forwardRef< export function PressableWithHover({
View,
PropsWithChildren<PressableWithHover>
>(function PressableWithHoverImpl(
{children, style, hoverStyle, ...props},
ref, ref,
) { children,
style,
hoverStyle,
...props
}: PressableWithHoverProps) {
const { const {
state: hovered, state: hovered,
onIn: onHoverIn, onIn: onHoverIn,
@@ -41,4 +34,4 @@ export const PressableWithHover = forwardRef<
{children} {children}
</Pressable> </Pressable>
) )
}) }
+4 -8
View File
@@ -1,7 +1,6 @@
import {forwardRef, memo} from 'react' import {forwardRef, memo} from 'react'
import {type FlatListComponent, View, type ViewProps} from 'react-native' import {View, type ViewProps} from 'react-native'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {type FlatListPropsWithLayout} from 'react-native-reanimated'
// If you explode these into functions, don't forget to forwardRef! // If you explode these into functions, don't forget to forwardRef!
@@ -10,22 +9,19 @@ import {type FlatListPropsWithLayout} from 'react-native-reanimated'
* The types are a bit wrong on `FlatList_INTERNAL` * The types are a bit wrong on `FlatList_INTERNAL`
*/ */
export const FlatList_INTERNAL = memo(Animated.FlatList) export const FlatList_INTERNAL = memo(Animated.FlatList)
export type FlatList_INTERNAL<ItemT = any> = Omit< export type FlatList_INTERNAL = React.ComponentRef<typeof Animated.FlatList>
FlatListComponent<ItemT, FlatListPropsWithLayout<ItemT>>,
'CellRendererComponent'
>
/** /**
* @deprecated use `Layout` components * @deprecated use `Layout` components
*/ */
export const ScrollView = Animated.ScrollView export const ScrollView = Animated.ScrollView
export type ScrollView = typeof Animated.ScrollView export type ScrollView = React.ComponentRef<typeof Animated.ScrollView>
/** /**
* @deprecated use `Layout` components * @deprecated use `Layout` components
*/ */
export const CenteredView = forwardRef< export const CenteredView = forwardRef<
View, React.ComponentRef<typeof View>,
React.PropsWithChildren< React.PropsWithChildren<
ViewProps & {sideBorders?: boolean; topBorder?: boolean} ViewProps & {sideBorders?: boolean; topBorder?: boolean}
> >
+3 -7
View File
@@ -14,7 +14,6 @@
import {forwardRef} from 'react' import {forwardRef} from 'react'
import { import {
type FlatList,
type FlatListProps, type FlatListProps,
type ScrollViewProps, type ScrollViewProps,
StyleSheet, StyleSheet,
@@ -45,7 +44,7 @@ export const CenteredView = forwardRef(function CenteredView(
}: React.PropsWithChildren< }: React.PropsWithChildren<
ViewProps & {sideBorders?: boolean; topBorder?: boolean} ViewProps & {sideBorders?: boolean; topBorder?: boolean}
>, >,
ref: React.Ref<View>, ref: React.Ref<React.ComponentRef<typeof View>>,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
@@ -76,7 +75,7 @@ export const FlatList_INTERNAL = forwardRef(function FlatListImpl<ItemT>(
}: React.PropsWithChildren< }: React.PropsWithChildren<
Omit<FlatListProps<ItemT>, 'CellRendererComponent'> & AddedProps Omit<FlatListProps<ItemT>, 'CellRendererComponent'> & AddedProps
>, >,
ref: React.Ref<FlatList<ItemT>>, ref: React.Ref<React.ComponentRef<typeof Animated.FlatList>>,
) { ) {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {centerColumnOffset} = useLayoutBreakpoints() const {centerColumnOffset} = useLayoutBreakpoints()
@@ -105,7 +104,6 @@ export const FlatList_INTERNAL = forwardRef(function FlatListImpl<ItemT>(
} }
if (desktopFixedHeight) { if (desktopFixedHeight) {
if (typeof desktopFixedHeight === 'number') { if (typeof desktopFixedHeight === 'number') {
// @ts-expect-error Web only -prf
style = addStyle(style, { style = addStyle(style, {
height: `calc(100vh - ${desktopFixedHeight}px)`, height: `calc(100vh - ${desktopFixedHeight}px)`,
}) })
@@ -143,7 +141,7 @@ export const FlatList_INTERNAL = forwardRef(function FlatListImpl<ItemT>(
*/ */
export const ScrollView = forwardRef(function ScrollViewImpl( export const ScrollView = forwardRef(function ScrollViewImpl(
{contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
ref: React.Ref<Animated.ScrollView>, ref: React.Ref<React.ComponentRef<typeof Animated.ScrollView>>,
) { ) {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {centerColumnOffset} = useLayoutBreakpoints() const {centerColumnOffset} = useLayoutBreakpoints()
@@ -170,7 +168,6 @@ export const ScrollView = forwardRef(function ScrollViewImpl(
const styles = StyleSheet.create({ const styles = StyleSheet.create({
contentContainer: { contentContainer: {
// @ts-expect-error web only
minHeight: '100vh', minHeight: '100vh',
}, },
container: { container: {
@@ -189,7 +186,6 @@ const styles = StyleSheet.create({
marginRight: 'auto', marginRight: 'auto',
}, },
fixedHeight: { fixedHeight: {
// @ts-expect-error web only
height: '100vh', height: '100vh',
}, },
}) })
+4 -11
View File
@@ -4,8 +4,6 @@ import {
type GestureResponderEvent, type GestureResponderEvent,
type NativeSyntheticEvent, type NativeSyntheticEvent,
type NativeTouchEvent, type NativeTouchEvent,
Pressable,
type PressableStateCallbackType,
type StyleProp, type StyleProp,
StyleSheet, StyleSheet,
type TextStyle, type TextStyle,
@@ -15,6 +13,10 @@ import {
import {choose} from '#/lib/functions' import {choose} from '#/lib/functions'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {
Pressable,
type PressableStateCallbackType,
} from '#/components/Pressable'
import {Text} from '../text/Text' import {Text} from '../text/Text'
export type ButtonType = export type ButtonType =
@@ -28,15 +30,6 @@ export type ButtonType =
| 'secondary-light' | 'secondary-light'
| 'default-light' | 'default-light'
// Augment type for react-native-web (see https://github.com/necolas/react-native-web/issues/1684#issuecomment-766451866)
declare module 'react-native' {
interface PressableStateCallbackType {
// @ts-ignore web only
hovered?: boolean
focused?: boolean
}
}
/** /**
* @deprecated use Button from `#/components/Button.tsx` instead * @deprecated use Button from `#/components/Button.tsx` instead
*/ */
+1 -1
View File
@@ -8,5 +8,5 @@ import {type ListMethods} from './List'
*/ */
export function findListNativeTag(list: ListMethods | null): number | null { export function findListNativeTag(list: ListMethods | null): number | null {
if (!list) return null if (!list) return null
return findNodeHandle(list) return findNodeHandle(list) ?? null
} }
+5 -7
View File
@@ -5,7 +5,7 @@ import {UITextView} from '@bsky.app/react-native-uitextview'
import {lh, s} from '#/lib/styles' import {lh, s} from '#/lib/styles'
import {type TypographyVariant, useTheme} from '#/lib/ThemeContext' import {type TypographyVariant, useTheme} from '#/lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {applyFonts, useAlf} from '#/alf' import {applyFonts, type MutableTextStyle, useAlf} from '#/alf'
import { import {
childHasEmoji, childHasEmoji,
renderChildrenWithEmoji, renderChildrenWithEmoji,
@@ -61,12 +61,10 @@ function Text_DEPRECATED({
const typography = theme.typography[type] const typography = theme.typography[type]
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
const flattened = StyleSheet.flatten([ const flattened: MutableTextStyle = {
s.black, ...(StyleSheet.flatten([s.black, typography, lineHeightStyle, style]) ??
typography, {}),
lineHeightStyle, }
style,
])
applyFonts(flattened, fonts.family) applyFonts(flattened, fonts.family)
+1 -1
View File
@@ -262,7 +262,7 @@ function NotificationsTab({
ListHeaderComponent={ ListHeaderComponent={
filter === 'mentions' ? ( filter === 'mentions' ? (
<DisabledNotificationsWarning active={isFocusedAndActive} /> <DisabledNotificationsWarning active={isFocusedAndActive} />
) : null ) : undefined
} }
/> />
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (

Some files were not shown because too many files have changed in this diff Show More