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 (