diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index 2a1d3f3293..44a2c985f5 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -56,6 +56,16 @@ jest.mock('expo-media-library', () => ({
__esModule: true, // this property makes it work
default: jest.fn(),
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', () => ({
diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json
index e04604a747..d455f05f4b 100644
--- a/oxlint-suppressions.json
+++ b/oxlint-suppressions.json
@@ -80,14 +80,6 @@
"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": {
"typescript/no-floating-promises": {
"count": 2
@@ -167,9 +159,6 @@
"typescript/no-explicit-any": {
"count": 4
},
- "typescript/no-misused-promises": {
- "count": 1
- },
"typescript/no-unsafe-call": {
"count": 2
},
@@ -224,9 +213,6 @@
"src/components/Lists.tsx": {
"typescript/no-explicit-any": {
"count": 1
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/components/Menu/types.ts": {
@@ -261,9 +247,6 @@
"src/components/PostControls/BookmarkButton.tsx": {
"typescript/no-explicit-any": {
"count": 2
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/components/PostControls/DiscoverDebug.tsx": {
@@ -335,9 +318,6 @@
"typescript/no-floating-promises": {
"count": 5
},
- "typescript/no-misused-promises": {
- "count": 2
- },
"typescript/require-await": {
"count": 4
}
@@ -346,9 +326,6 @@
"typescript/no-floating-promises": {
"count": 1
},
- "typescript/no-misused-promises": {
- "count": 2
- },
"typescript/require-await": {
"count": 1
}
@@ -363,16 +340,6 @@
"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": {
"typescript/require-await": {
"count": 1
@@ -388,18 +355,10 @@
"count": 1
}
},
- "src/components/contacts/screens/ViewMatches.tsx": {
- "typescript/no-misused-promises": {
- "count": 1
- }
- },
"src/components/dialogs/DeviceLocationRequestDialog.tsx": {
"typescript/no-explicit-any": {
"count": 1
},
- "typescript/no-misused-promises": {
- "count": 1
- },
"typescript/no-unsafe-member-access": {
"count": 2
}
@@ -409,26 +368,6 @@
"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": {
"typescript/no-explicit-any": {
"count": 1
@@ -447,7 +386,7 @@
"count": 2
},
"typescript/no-misused-promises": {
- "count": 3
+ "count": 1
},
"typescript/no-unsafe-member-access": {
"count": 2
@@ -490,9 +429,6 @@
"src/components/dialogs/lists/CreateOrEditListDialog.tsx": {
"typescript/no-explicit-any": {
"count": 2
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/components/dialogs/nuxs/FindContactsAnnouncement.tsx": {
@@ -580,16 +516,6 @@
"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": {
"typescript/no-misused-promises": {
"count": 1
@@ -696,13 +622,10 @@
"count": 1
},
"typescript/no-misused-promises": {
- "count": 3
+ "count": 2
}
},
"src/lib/hooks/usePermissions.ts": {
- "typescript/no-misused-promises": {
- "count": 1
- },
"typescript/no-unsafe-enum-comparison": {
"count": 5
}
@@ -848,12 +771,12 @@
},
"typescript/no-misused-promises": {
"count": 1
+ },
+ "typescript/no-unsafe-member-access": {
+ "count": 1
}
},
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
- "typescript/no-misused-promises": {
- "count": 6
- },
"typescript/require-await": {
"count": 6
}
@@ -863,22 +786,9 @@
"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": {
"typescript/no-floating-promises": {
"count": 3
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/screens/Log.tsx": {
@@ -894,7 +804,7 @@
"count": 1
},
"typescript/no-misused-promises": {
- "count": 2
+ "count": 1
},
"typescript/no-unsafe-member-access": {
"count": 2
@@ -922,19 +832,6 @@
"src/screens/Onboarding/StepProfile/index.tsx": {
"typescript/no-floating-promises": {
"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": {
@@ -962,18 +859,12 @@
"src/screens/Profile/Header/EditProfileDialog.tsx": {
"typescript/no-explicit-any": {
"count": 3
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/screens/Profile/Header/ProfileHeaderLabeler.tsx": {
"typescript/no-explicit-any": {
"count": 2
},
- "typescript/no-misused-promises": {
- "count": 1
- },
"typescript/no-unsafe-member-access": {
"count": 3
}
@@ -1004,11 +895,6 @@
"count": 1
}
},
- "src/screens/ProfileList/components/Header.tsx": {
- "typescript/no-misused-promises": {
- "count": 3
- }
- },
"src/screens/ProfileList/components/MoreOptionsMenu.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1027,11 +913,6 @@
"count": 1
}
},
- "src/screens/SavedFeeds.tsx": {
- "typescript/no-misused-promises": {
- "count": 2
- }
- },
"src/screens/Search/modules/ExploreSuggestedAccounts.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1096,6 +977,12 @@
},
"typescript/no-misused-promises": {
"count": 2
+ },
+ "typescript/no-unsafe-call": {
+ "count": 2
+ },
+ "typescript/no-unsafe-member-access": {
+ "count": 2
}
},
"src/screens/Settings/components/CopyButton.tsx": {
@@ -1107,9 +994,6 @@
"typescript/no-explicit-any": {
"count": 1
},
- "typescript/no-misused-promises": {
- "count": 1
- },
"typescript/no-unsafe-member-access": {
"count": 1
}
@@ -1119,11 +1003,6 @@
"count": 2
}
},
- "src/screens/Settings/components/DisableEmail2FADialog.tsx": {
- "typescript/no-misused-promises": {
- "count": 4
- }
- },
"src/screens/Settings/components/OTAInfo.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1147,7 +1026,7 @@
"count": 1
},
"typescript/no-misused-promises": {
- "count": 2
+ "count": 1
},
"typescript/no-unsafe-call": {
"count": 1
@@ -1168,9 +1047,6 @@
"typescript/no-floating-promises": {
"count": 1
},
- "typescript/no-misused-promises": {
- "count": 2
- },
"typescript/require-await": {
"count": 1
}
@@ -1474,9 +1350,6 @@
"src/view/com/composer/SelectMediaButton.tsx": {
"typescript/no-floating-promises": {
"count": 1
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/view/com/composer/drafts/DraftsButton.tsx": {
@@ -1500,11 +1373,6 @@
"count": 2
}
},
- "src/view/com/composer/photos/EditImageDialog.web.tsx": {
- "typescript/no-misused-promises": {
- "count": 1
- }
- },
"src/view/com/composer/photos/Gallery.tsx": {
"typescript/no-floating-promises": {
"count": 1
@@ -1513,9 +1381,6 @@
"src/view/com/composer/photos/OpenCameraBtn.tsx": {
"typescript/no-explicit-any": {
"count": 1
- },
- "typescript/no-misused-promises": {
- "count": 1
}
},
"src/view/com/composer/text-input/TextInput.web.tsx": {
@@ -1560,7 +1425,7 @@
"count": 2
},
"typescript/no-misused-promises": {
- "count": 3
+ "count": 2
},
"typescript/no-unsafe-member-access": {
"count": 6
@@ -1598,12 +1463,6 @@
"src/view/com/pager/TabBar.web.tsx": {
"typescript/no-explicit-any": {
"count": 1
- },
- "typescript/no-unsafe-call": {
- "count": 2
- },
- "typescript/no-unsafe-member-access": {
- "count": 4
}
},
"src/view/com/post-thread/PostLikedBy.tsx": {
@@ -1627,9 +1486,6 @@
"src/view/com/posts/FeedShutdownMsg.tsx": {
"typescript/no-explicit-any": {
"count": 2
- },
- "typescript/no-misused-promises": {
- "count": 2
}
},
"src/view/com/posts/PostFeedErrorMessage.tsx": {
@@ -1693,9 +1549,6 @@
"src/view/com/util/Views.tsx": {
"react/display-name": {
"count": 1
- },
- "typescript/no-explicit-any": {
- "count": 1
}
},
"src/view/com/util/Views.web.tsx": {
@@ -1703,11 +1556,6 @@
"count": 1
}
},
- "src/view/com/util/forms/Button.tsx": {
- "typescript/no-misused-promises": {
- "count": 1
- }
- },
"src/view/screens/Feeds.tsx": {
"typescript/no-floating-promises": {
"count": 3
@@ -1771,4 +1619,4 @@
"count": 3
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Splash.tsx b/src/Splash.tsx
index 4a4dea831a..4cc50b4ebb 100644
--- a/src/Splash.tsx
+++ b/src/Splash.tsx
@@ -24,10 +24,11 @@ import {atoms as a} from '#/alf'
import splashImagePointer from '../assets/splash/splash.png'
// @ts-ignore
import darkSplashImagePointer from '../assets/splash/splash-dark.png'
-const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
+
+const splashImageUri = RNImage.resolveAssetSource(splashImagePointer)!.uri
const darkSplashImageUri = RNImage.resolveAssetSource(
darkSplashImagePointer,
-).uri
+)!.uri
export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) {
const width = 1000
diff --git a/src/Splash.web.tsx b/src/Splash.web.tsx
index ff86c9a770..44e14baa33 100644
--- a/src/Splash.web.tsx
+++ b/src/Splash.web.tsx
@@ -7,7 +7,7 @@
import {useEffect, useRef, useState} from 'react'
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 ratio = 57 / 64
@@ -72,7 +72,7 @@ export function Splash({
{!isAnimationComplete && (
,
) {
- const s = flatten(styles) ?? {}
+ const s: MutableTextStyle = {...flatten(styles)}
// should always be defined on these components
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
diff --git a/src/alf/util/dimensions.ts b/src/alf/util/dimensions.ts
index 31af2ffa99..e5d60055c1 100644
--- a/src/alf/util/dimensions.ts
+++ b/src/alf/util/dimensions.ts
@@ -1,5 +1,5 @@
import {useEffect, useState} from 'react'
-import {Dimensions} from 'react-native'
+import {Dimensions, type DimensionsPayload} from 'react-native'
/**
* Same as `useWindowDimensions().fontScale`, but avoids rerendering
@@ -9,9 +9,12 @@ export function useNativeFontScale() {
const [fontScale, setFontScale] = useState(Dimensions.get('window').fontScale)
useEffect(() => {
- const sub = Dimensions.addEventListener('change', evt => {
- setFontScale(evt.window.fontScale)
- })
+ const sub = Dimensions.addEventListener(
+ 'change',
+ (evt: DimensionsPayload) => {
+ if (evt.window) setFontScale(evt.window.fontScale)
+ },
+ )
return () => sub.remove()
}, [])
diff --git a/src/alf/util/flatten.ts b/src/alf/util/flatten.ts
index 6d49ce6e51..b218c8879e 100644
--- a/src/alf/util/flatten.ts
+++ b/src/alf/util/flatten.ts
@@ -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
(
+ style?: StyleProp,
+): T extends (infer U)[] ? U : T {
+ return (StyleSheet.flatten(
+ style as unknown as Parameters[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[0],
+ ) ?? {}) as React.CSSProperties
+}
/**
* 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.
*/
export function extractPadding(style: PaddingStyle | PaddingStyle[]) {
- const s = flatten(style as any) ?? {}
+ const s = flatten(style)
const base = num(s.padding)
return {
paddingTop: num(s.paddingTop) || num(s.paddingVertical) || base,
diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts
index 7cb9b12723..5dae61c630 100644
--- a/src/alf/util/useColorModeTheme.ts
+++ b/src/alf/util/useColorModeTheme.ts
@@ -24,7 +24,7 @@ export function useThemeName(): ThemeName {
}
function getThemeName(
- colorScheme: ColorSchemeName,
+ colorScheme: ColorSchemeName | null | undefined,
colorMode: 'system' | 'light' | 'dark',
darkTheme?: ThemeName,
) {
diff --git a/src/components/AltBadgeWithDialog.tsx b/src/components/AltBadgeWithDialog.tsx
index 07cf9544f0..4ae078e12b 100644
--- a/src/components/AltBadgeWithDialog.tsx
+++ b/src/components/AltBadgeWithDialog.tsx
@@ -1,9 +1,9 @@
-import {Pressable} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_20} from '#/lib/constants'
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
import {atoms as a, useTheme} from '#/alf'
+import {Pressable} from '#/components/Pressable'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
@@ -49,7 +49,7 @@ export function AltBadgeWithDialog({
accessibilityHint=""
hitSlop={HITSLOP_20}
onPress={control.open}
- style={s => [
+ style={({pressed, hovered}) => [
a.justify_center,
a.rounded_sm,
a.p_xs,
@@ -62,7 +62,7 @@ export function AltBadgeWithDialog({
opacity: 0.8,
},
pos,
- s.hovered || s.pressed
+ hovered || pressed
? [
{
opacity: 1,
diff --git a/src/components/BetaBadge.tsx b/src/components/BetaBadge.tsx
index 115f8d0046..c8c0ddd2bf 100644
--- a/src/components/BetaBadge.tsx
+++ b/src/components/BetaBadge.tsx
@@ -1,11 +1,12 @@
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 {usePreferencesQuery} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {atoms as a, useTheme} from '#/alf'
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
+import {Pressable} from '#/components/Pressable'
import * as Tooltip from '#/components/Tooltip'
import type * as bsky from '#/types/bsky'
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 54332fbc07..eee55647e0 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -101,6 +101,8 @@ export type ButtonProps = Pick<
| 'onPressOut'
| 'onFocus'
| 'onBlur'
+ | 'onAccessibilityAction'
+ | 'onAccessibilityEscape'
> &
AccessibilityProps &
VariantProps & {
@@ -131,7 +133,7 @@ export function useButtonContext() {
return useContext(Context)
}
-export const Button = forwardRef(
+export const Button = forwardRef, ButtonProps>(
(
{
children,
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index f4a0302ff7..0f16496fc6 100644
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -39,6 +39,9 @@ import {
import {Span, Text} from '#/components/Typography'
import {IS_IOS, IS_WEB, IS_WEB_TOUCH_DEVICE} from '#/env'
+type TextInputInstance = React.ComponentRef
+type ViewInstance = React.ComponentRef
+
export type SubmitRequest =
| {
platform: 'web'
@@ -60,7 +63,7 @@ export type ComposerInternalApi = {
input?: ReturnType['input']
clear: () => void
insert(text: string): void
- setAutocompleteAnchor: (node: View | null) => void
+ setAutocompleteAnchor: (node: ViewInstance | null) => void
}
export function useComposerInternalApiRef() {
@@ -82,7 +85,7 @@ export type ComposerProps = Omit<
| 'onSubmitEditing'
> & {
label: string
- ref?: React.RefObject
+ ref?: React.RefObject
internalApiRef?: React.Ref
outerStyle?: ViewStyleProp['style']
contentTextStyle?: TextStyleProp['style']
@@ -139,6 +142,11 @@ export function Composer({
placement: autocompletePlacement,
dynamicWidth: IS_WEB,
})
+ const inputRef = mergeRefs([
+ ref,
+ tapper.inputProps.ref as React.Ref,
+ sift.targetProps.ref as React.Ref,
+ ])
/*
* Active facet state for controlling the visibility of the Autocomplete.
@@ -345,7 +353,7 @@ export function Composer({
{...rest}
{...tapper.inputProps}
{...sift.targetProps}
- ref={mergeRefs([ref, tapper.inputProps.ref, sift.targetProps.ref])}
+ ref={inputRef}
rawValue={tapper.state.text}
onBlur={e => {
rest.onBlur?.(e)
diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx
index d09e0e60ec..95492a385a 100644
--- a/src/components/ContextMenu/index.tsx
+++ b/src/components/ContextMenu/index.tsx
@@ -244,7 +244,7 @@ export function Trigger({
const context = useContextMenuContext()
const playHaptic = useHaptics()
const insets = useSafeAreaInsets()
- const ref = useRef(null)
+ const ref = useRef>(null)
const isFocused = useIsFocused()
const [image, setImage] = useState(null)
const [pendingMeasurement, setPendingMeasurement] = useState<{
@@ -971,7 +971,10 @@ export function Divider() {
)
}
-function measureView(view: View | null, insets: EdgeInsets) {
+function measureView(
+ view: React.ComponentRef | null,
+ insets: EdgeInsets,
+) {
if (!view) return Promise.resolve(null)
return new Promise(resolve => {
view?.measureInWindow((x, y, width, height) =>
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index 4f6984a4a0..5cfa2d1c10 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -8,7 +8,7 @@ import {
} from 'react'
import {
Keyboard,
- type KeyboardEventListener,
+ type KeyboardEvent,
type LayoutChangeEvent,
type NativeScrollEvent,
type NativeSyntheticEvent,
@@ -203,80 +203,85 @@ export function Inner(props: DialogInnerProps) {
return
}
-export const ScrollableInner = forwardRef(
- function ScrollableInner(
- {children, contentContainerStyle, header, footer, style, ...props},
- ref,
- ) {
- 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,
- )
+export function ScrollableInner({
+ ref,
+ children,
+ contentContainerStyle,
+ header,
+ footer,
+ style,
+ ...props
+}: DialogInnerProps & {
+ ref?: React.Ref>
+}) {
+ 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(e => {
- setKeyboardHeight(e.endCoordinates.height)
- }, [])
- useOnKeyboard('keyboardDidShow', keyboardEventHandler)
- useOnKeyboard('keyboardDidHide', keyboardEventHandler)
+ const keyboardEventHandler = useCallback((e: KeyboardEvent) => {
+ setKeyboardHeight(e.endCoordinates.height)
+ }, [])
+ useOnKeyboard('keyboardDidShow', keyboardEventHandler)
+ useOnKeyboard('keyboardDidHide', keyboardEventHandler)
- const onScroll = (e: NativeSyntheticEvent) => {
- if (!IS_ANDROID) {
- return
- }
- const {contentOffset} = e.nativeEvent
- if (contentOffset.y > 0 && !disableDrag) {
- setDisableDrag(true)
- } else if (contentOffset.y <= 1 && disableDrag) {
- setDisableDrag(false)
- }
+ const onScroll = (e: NativeSyntheticEvent) => {
+ if (!IS_ANDROID) {
+ return
}
+ const {contentOffset} = e.nativeEvent
+ if (contentOffset.y > 0 && !disableDrag) {
+ setDisableDrag(true)
+ } else if (contentOffset.y <= 1 && disableDrag) {
+ setDisableDrag(false)
+ }
+ }
- return (
- <>
-
- {header}
- {children}
-
- {footer}
- >
- )
- },
-)
+ return (
+ <>
+
+ {header}
+ {children}
+
+ {footer}
+ >
+ )
+}
export const InnerFlatList = forwardRef<
ListMethods,
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index c26a71825b..e867acd60a 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -193,7 +193,6 @@ export function Inner({
aria-label={label}
aria-labelledby={accessibilityLabelledBy}
aria-describedby={accessibilityDescribedBy}
- // @ts-expect-error web only -prf
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
@@ -239,7 +238,9 @@ export function Inner({
export function ScrollableInner({
ref: _ref,
...props
-}: DialogInnerProps & {ref?: React.Ref}) {
+}: DialogInnerProps & {
+ ref?: React.Ref>
+}) {
return
}
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index aa77a7b36b..a868004195 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -217,7 +217,7 @@ export function ProfileGrid({
// Track seen profiles
const seenProfilesRef = useRef>(new Set())
- const containerRef = useRef(null)
+ const containerRef = useRef>(null)
const hasTrackedRef = useRef(false)
const logContext: Metrics['suggestedUser:seen']['logContext'] = isFeedContext
? 'DiscoverInterstitial'
diff --git a/src/components/FocusScope/index.tsx b/src/components/FocusScope/index.tsx
index 48e013500e..598c3cd6ca 100644
--- a/src/components/FocusScope/index.tsx
+++ b/src/components/FocusScope/index.tsx
@@ -41,7 +41,7 @@ export function FocusScope({children}: {children: React.ReactNode}) {
*/
function FocusTrap({children}: {children: React.ReactNode}) {
const {_} = useLingui()
- const child = useRef(null)
+ const child = useRef>(null)
/*
* 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])
- const focusNode = useCallback((ref: View | null) => {
- if (!ref) return
- const node = findNodeHandle(ref)
- if (node) {
- AccessibilityInfo.setAccessibilityFocus(node)
- }
- }, [])
+ const focusNode = useCallback(
+ (ref: React.ComponentRef | null) => {
+ if (!ref) return
+ const node = findNodeHandle(ref)
+ if (node) {
+ AccessibilityInfo.setAccessibilityFocus(node)
+ }
+ },
+ [],
+ )
useEffect(() => {
setTimeout(() => {
diff --git a/src/components/GlassView.tsx b/src/components/GlassView.tsx
index cac5bd268d..ddbcc07d80 100644
--- a/src/components/GlassView.tsx
+++ b/src/components/GlassView.tsx
@@ -20,7 +20,7 @@ export const IS_GLASS_AVAILABLE =
*/
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
-export type GlassViewProps = ExpoGlassViewProps & {
+export type GlassViewProps = Omit & {
fallbackStyle?: StyleProp
}
diff --git a/src/components/InterestTabs.tsx b/src/components/InterestTabs.tsx
index 5df1fb62e3..5a99a36992 100644
--- a/src/components/InterestTabs.tsx
+++ b/src/components/InterestTabs.tsx
@@ -47,7 +47,7 @@ export function InterestTabs({
}) {
const t = useTheme()
const {_} = useLingui()
- const listRef = useRef(null)
+ const listRef = useRef>(null)
const [totalWidth, setTotalWidth] = useState(0)
const [scrollX, setScrollX] = useState(0)
const [contentWidth, setContentWidth] = useState(0)
diff --git a/src/components/Layout/Header/index.tsx b/src/components/Layout/Header/index.tsx
index a0bd45c8fc..c70cebd703 100644
--- a/src/components/Layout/Header/index.tsx
+++ b/src/components/Layout/Header/index.tsx
@@ -42,7 +42,7 @@ export function Outer({
}: {
children: React.ReactNode
noBottomBorder?: boolean
- headerRef?: React.RefObject
+ headerRef?: React.RefObject | null>
sticky?: boolean
}) {
const t = useTheme()
diff --git a/src/components/Lightbox/chrome/ImageMenu.tsx b/src/components/Lightbox/chrome/ImageMenu.tsx
index 9088b9ded9..14ce100c0f 100644
--- a/src/components/Lightbox/chrome/ImageMenu.tsx
+++ b/src/components/Lightbox/chrome/ImageMenu.tsx
@@ -35,7 +35,7 @@ const TIMING_OUT = {duration: 150}
export function ImageMenu({onPressShare, onPressSave}: Props) {
const {t: l} = useLingui()
- const triggerRef = useRef(null)
+ const triggerRef = useRef>(null)
const [isMounted, setIsMounted] = useState(false)
const [anchor, setAnchor] = useState(null)
const progress = useSharedValue(0)
diff --git a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
index 9cf088f864..6da83e7f7a 100644
--- a/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
+++ b/src/components/Lightbox/pager/ImageItem/ImageItem.ios.tsx
@@ -82,7 +82,7 @@ const ImageItem = ({
const scrollHandler = useAnimatedScrollHandler({
onScroll(e) {
'worklet'
- const nextIsScaled = e.zoomScale > 1
+ const nextIsScaled = (e.zoomScale ?? 1) > 1
if (scaled !== nextIsScaled) {
scheduleOnRN(handleZoom, nextIsScaled)
}
diff --git a/src/components/Lightbox/pager/ImagePager.tsx b/src/components/Lightbox/pager/ImagePager.tsx
index 28c75be2cc..d89bb74d85 100644
--- a/src/components/Lightbox/pager/ImagePager.tsx
+++ b/src/components/Lightbox/pager/ImagePager.tsx
@@ -11,7 +11,10 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
import {PixelRatio, StyleSheet, useWindowDimensions, View} from 'react-native'
import {SystemBars} from 'react-native-edge-to-edge'
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, {
type AnimatableValue,
type AnimatedRef,
@@ -383,7 +386,7 @@ function ImageView({
{
+ onPageSelected={(e: PagerViewOnPageSelectedEvent) => {
const next = e.nativeEvent.position
setImageIndex(prev => {
if (metricsContext && prev !== next) {
@@ -401,7 +404,7 @@ function ImageView({
})
setIsScaled(false)
}}
- onPageScrollStateChanged={e => {
+ onPageScrollStateChanged={(e: PageScrollStateChangedNativeEvent) => {
setIsDragging(e.nativeEvent.pageScrollState !== 'idle')
}}
overdrag={true}
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
index 6d60edb615..8e5891f3b2 100644
--- a/src/components/Link.tsx
+++ b/src/components/Link.tsx
@@ -478,7 +478,7 @@ export function InlineLinkText({
onIn: onInteract,
onOut: onInteractOut,
} = useInteractionState()
- const flattenedStyle = flatten(style) || {}
+ const flattenedStyle = flatten(style)
return (
void
+ onKeyDown: PressableProps['onKeyDown']
/**
* Radix provides this, but we override on web to use `onPress` instead,
* which is less sensitive while scrolling.
diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx
index 849222df03..6d44e9ce75 100644
--- a/src/components/Post/Translated/index.tsx
+++ b/src/components/Post/Translated/index.tsx
@@ -267,7 +267,7 @@ function TranslationResult({
? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined
- const flattenedStyle = flatten(postTextStyle) ?? {}
+ const flattenedStyle = flatten(postTextStyle)
const fontSize = flattenedStyle.fontSize
return (
diff --git a/src/components/PostControls/PostControlButton.tsx b/src/components/PostControls/PostControlButton.tsx
index 3ea85e2811..e65a931f7d 100644
--- a/src/components/PostControls/PostControlButton.tsx
+++ b/src/components/PostControls/PostControlButton.tsx
@@ -27,7 +27,7 @@ export function PostControlButton({
activeColor,
...props
}: Omit & {
- ref?: React.Ref
+ ref?: React.Ref>
active?: boolean
big?: boolean
color?: string
diff --git a/src/components/Pressable.tsx b/src/components/Pressable.tsx
new file mode 100644
index 0000000000..cb6920bc0e
--- /dev/null
+++ b/src/components/Pressable.tsx
@@ -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,
+ 'children' | 'style'
+> & {
+ children?:
+ | React.ReactNode
+ | ((state: PressableStateCallbackType) => React.ReactNode)
+ style?:
+ | StyleProp
+ | ((state: PressableStateCallbackType) => StyleProp)
+}
+
+/**
+ * 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 (
+
+ {children}
+
+ )
+}
diff --git a/src/components/ProgressGuide/FollowDialog.tsx b/src/components/ProgressGuide/FollowDialog.tsx
index d7fa9c1fab..1ab2668ae5 100644
--- a/src/components/ProgressGuide/FollowDialog.tsx
+++ b/src/components/ProgressGuide/FollowDialog.tsx
@@ -1,5 +1,5 @@
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 {Trans, useLingui} from '@lingui/react/macro'
@@ -139,7 +139,7 @@ function DialogInner({guide}: {guide?: Follow10ProgressGuide}) {
const [searchText, setSearchText] = useState(lastSearchText)
const moderationOpts = useModerationOpts()
const listRef = useRef(null)
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [headerHeight, setHeaderHeight] = useState(0)
const {currentAccount} = useSession()
@@ -374,7 +374,7 @@ let Header = ({
interestsDisplayNames,
}: {
guide?: Follow10ProgressGuide
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
listRef: React.RefObject
onSelectTab: (v: string) => void
searchText: string
@@ -679,7 +679,7 @@ function SearchInput({
}: {
onChangeText: (text: string) => void
onEscape: () => void
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
defaultValue: string
}) {
const t = useTheme()
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 0266dae0ce..00a0cd876d 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -93,7 +93,7 @@ export function RichText({
if (!facets?.length) {
if (isOnlyEmoji(text)) {
- const flattenedStyle = flatten(style) ?? {}
+ const flattenedStyle = flatten(style)
const fontSize =
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return (
diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx
index a7dd70d8e7..af5849346b 100644
--- a/src/components/Select/index.tsx
+++ b/src/components/Select/index.tsx
@@ -100,7 +100,16 @@ export function Trigger({children, hitSlop, label}: TriggerProps) {
} else {
return (
@@ -273,7 +273,7 @@ export function Item({ref, value, style, children}: ItemProps) {
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
- style={flatten([
+ style={flattenToCSS([
t.atoms.text,
a.relative,
a.flex,
@@ -307,7 +307,7 @@ export const ItemText = function ItemText({children, style}: ItemTextProps) {
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
return (
- >
moderationOpts: ModerationOpts
headerHeight: number
scrollElRef: ListRef
diff --git a/src/components/Tooltip/index.tsx b/src/components/Tooltip/index.tsx
index 4f89950527..350bf8b286 100644
--- a/src/components/Tooltip/index.tsx
+++ b/src/components/Tooltip/index.tsx
@@ -27,8 +27,9 @@ import {
import {Text} from '#/components/Typography'
const TooltipPortal = createPortalGroup()
-const TooltipProviderContext =
- createContext | null>(null)
+const TooltipProviderContext = createContext | null> | null>(null)
/**
* 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.
*/
export function SheetCompatProvider({children}: {children: React.ReactNode}) {
- const ref = useRef(null)
+ const ref = useRef | null>(null)
return (
@@ -154,7 +155,7 @@ export function Outer({
export function Target({children}: {children: React.ReactNode}) {
const {shouldMeasure, setTargetMeasurements} = useContext(TargetContext)
const [hasLaidOut, setHasLaidOut] = useState(false)
- const targetRef = useRef(null)
+ const targetRef = useRef>(null)
const containerRef = useContext(TooltipProviderContext)
const keyboardIsOpen = useIsKeyboardVisible()
diff --git a/src/components/Tooltip/index.web.tsx b/src/components/Tooltip/index.web.tsx
index fd017b43ca..8d6c9634ab 100644
--- a/src/components/Tooltip/index.web.tsx
+++ b/src/components/Tooltip/index.web.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
import {utils} from '@bsky.app/alf'
import {Popover} from 'radix-ui'
-import {atoms as a, flatten, useTheme} from '#/alf'
+import {atoms as a, flattenToCSS, useTheme} from '#/alf'
import {
ARROW_SIZE,
BUBBLE_MAX_WIDTH,
@@ -85,7 +85,7 @@ export function Content({
evt.preventDefault()
}
}}
- style={flatten([
+ style={flattenToCSS([
a.rounded_sm,
{
backgroundColor: style.surface,
diff --git a/src/components/contacts/components/OTPInput.tsx b/src/components/contacts/components/OTPInput.tsx
index 9834f6af9f..ef8944a575 100644
--- a/src/components/contacts/components/OTPInput.tsx
+++ b/src/components/contacts/components/OTPInput.tsx
@@ -25,13 +25,13 @@ export function OTPInput({
label: string
value: string
onChange: (text: string) => void
- ref?: React.Ref
+ ref?: React.Ref>
numberOfDigits?: number
onComplete?: (code: string) => void
}) {
const t = useTheme()
const {_} = useLingui()
- const innerRef = useRef(null)
+ const innerRef = useRef>(null)
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
const [selection, setSelection] = useState({start: 0, end: 0})
diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx
index 0d50bfdeeb..cba8f7092a 100644
--- a/src/components/contacts/screens/ViewMatches.tsx
+++ b/src/components/contacts/screens/ViewMatches.tsx
@@ -383,7 +383,7 @@ export function ViewMatches({
ref={listRef}
data={items}
renderItem={renderItem}
- ListFooterComponent={!isEmpty ? : null}
+ ListFooterComponent={!isEmpty ? : undefined}
keyExtractor={keyExtractor}
keyboardDismissMode="interactive"
automaticallyAdjustKeyboardInsets
diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx
index fdbdf0b7e3..3e405caef6 100644
--- a/src/components/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dialogs/SearchablePeopleList.tsx
@@ -97,7 +97,7 @@ export function SearchablePeopleList({
const [headerHeight, setHeaderHeight] = useState(0)
const listRef = useRef(null)
const {currentAccount} = useSession()
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [searchText, setSearchText] = useState('')
@@ -634,7 +634,7 @@ function SearchInput({
value: string
onChangeText: (text: string) => void
onEscape: () => void
- inputRef: React.RefObject
+ inputRef: React.RefObject | null>
}) {
const t = useTheme()
const {t: l} = useLingui()
diff --git a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
index de8f453d5a..2e171d9431 100644
--- a/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
+++ b/src/components/dialogs/lists/UserAddRemoveListsDialog.tsx
@@ -182,9 +182,9 @@ function ListsContent({
- ) : null
+ ) : undefined
}
- ListEmptyComponent={!isLoading && data ? : null}
+ ListEmptyComponent={!isLoading && data ? : undefined}
webInnerContentContainerStyle={[a.py_0]}
style={platform({
web: [a.px_2xl, a.pb_md],
diff --git a/src/components/dms/AddMembersFlow.tsx b/src/components/dms/AddMembersFlow.tsx
index bb591fb8c6..6a6b9fc49c 100644
--- a/src/components/dms/AddMembersFlow.tsx
+++ b/src/components/dms/AddMembersFlow.tsx
@@ -134,7 +134,7 @@ export function AddMembersFlow({
const [footerHeight, setFooterHeight] = useState(0)
const listRef = useRef(null)
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const [{groupChatDids, groupChatProfiles, searchText}, dispatch] = useReducer(
reducer,
@@ -502,7 +502,7 @@ export function AddMembersFlow({
- ) : null
+ ) : undefined
}
keyExtractor={(item: Item) => item.key}
style={[
diff --git a/src/components/dms/InitiateChatFlow.tsx b/src/components/dms/InitiateChatFlow.tsx
index 4b86eb5f0e..a0a80149d7 100644
--- a/src/components/dms/InitiateChatFlow.tsx
+++ b/src/components/dms/InitiateChatFlow.tsx
@@ -249,7 +249,7 @@ export function InitiateChatFlow({
const listRef = useRef(null)
const {currentAccount} = useSession()
const aa = useAgeAssurance()
- const inputRef = useRef(null)
+ const inputRef = useRef>(null)
const accountTooNewPromptControl = Dialog.useDialogControl()
const {data: convos} = useListConvosQuery({
diff --git a/src/components/dms/ReactionsDialog.tsx b/src/components/dms/ReactionsDialog.tsx
index 5a9f83c6e9..adaad953bb 100644
--- a/src/components/dms/ReactionsDialog.tsx
+++ b/src/components/dms/ReactionsDialog.tsx
@@ -255,7 +255,7 @@ function ReactionTabs({
const t = useTheme()
const {t: l} = useLingui()
- const scrollViewRef = useRef(null)
+ const scrollViewRef = useRef>(null)
const scrollState = useRef({x: 0, width: 0})
const tabLayouts = useRef