diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 20b60c57d1..49bc67ada9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,8 @@ jobs: strategy: fail-fast: false matrix: - job: [lint, prettier, typecheck] + job: + [lint, prettier, 'typecheck:ios', 'typecheck:android', 'typecheck:web'] steps: - name: Check out Git repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.web.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.web.tsx index 8149561a3a..2edea6ca96 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.web.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.web.tsx @@ -1,5 +1,14 @@ +import {Component} from 'react' + import {type BottomSheetViewProps} from './BottomSheet.types' -export function BottomSheetNativeComponent(_: BottomSheetViewProps) { - throw new Error('BottomSheetNativeComponent is not available on web') +export class BottomSheetNativeComponent extends Component { + /* + * Native sheets do not exist on web; there is nothing to dismiss. + */ + static dismissAll = async () => {} + + render(): never { + throw new Error('BottomSheetNativeComponent is not available on web') + } } diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts index 1bef0f74e9..5b0e566abd 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ios.ts @@ -6,7 +6,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise { + return Promise.resolve(getReferrerInfoSync()) +} + +function getReferrerInfoSync(): ReferrerInfo | null { const referrer = SharedPrefs.getString('referrer') if (referrer) { SharedPrefs.removeValue('referrer') diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts index 89ed4f58a3..3771e071de 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.ts @@ -5,6 +5,6 @@ export function getGooglePlayReferrerInfoAsync(): Promise { throw new NotImplementedError() } diff --git a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts index 3f8671ac31..d0510ef89f 100644 --- a/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts +++ b/modules/expo-bluesky-swiss-army/src/Referrer/index.web.ts @@ -7,7 +7,15 @@ export function getGooglePlayReferrerInfoAsync(): Promise { + return Promise.resolve(getReferrerInfoSync()) +} + +function getReferrerInfoSync(): ReferrerInfo | null { if ( Platform.OS === 'web' && // for ssr diff --git a/package.json b/package.json index c21d758187..3122d2d7bb 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,10 @@ "lint": "oxlint --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", - "typecheck": "tsgo --project ./tsconfig.check.json", + "typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web", + "typecheck:ios": "tsgo --project ./tsconfig.check.ios.json", + "typecheck:android": "tsgo --project ./tsconfig.check.android.json", + "typecheck:web": "tsgo --project ./tsconfig.check.web.json", "e2e:mock-server": "cd dev-env && pnpm start", "e2e:build": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build-android": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:android", diff --git a/patches/react-native-svg@15.12.1.patch b/patches/react-native-svg@15.12.1.patch index 299ac13106..25dbc13e8c 100644 --- a/patches/react-native-svg@15.12.1.patch +++ b/patches/react-native-svg@15.12.1.patch @@ -55,3 +55,15 @@ index 06829bd00dbded262e1871aaf6db3ae0cfa9d1b1..1b158185a6d7e907aa18ddc806902fcc invalidate(); } +diff --git a/lib/typescript/ReactNativeSVG.web.d.ts b/lib/typescript/ReactNativeSVG.web.d.ts +deleted file mode 100644 +index e2001975fa97398fb159d2f85f45d067200f575b..0000000000000000000000000000000000000000 +diff --git a/lib/typescript/ReactNativeSVG.web.d.ts.map b/lib/typescript/ReactNativeSVG.web.d.ts.map +deleted file mode 100644 +index c81af4416af5ee3eaca77b51bfc2952b30d49c8a..0000000000000000000000000000000000000000 +diff --git a/lib/typescript/elements.web.d.ts b/lib/typescript/elements.web.d.ts +deleted file mode 100644 +index cf4083fbf749b2b002fb649d80b33ac74763847b..0000000000000000000000000000000000000000 +diff --git a/lib/typescript/elements.web.d.ts.map b/lib/typescript/elements.web.d.ts.map +deleted file mode 100644 +index 29602aa47ad903bc7aaa111462a61dd862c4476d..0000000000000000000000000000000000000000 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe409d6149..563652a1fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,7 +231,7 @@ patchedDependencies: react-native-keyboard-controller@1.21.8: ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271 react-native-pager-view@6.8.0: 0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108 react-native-reanimated@3.19.1: 97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6 - react-native-svg@15.12.1: 31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9 + react-native-svg@15.12.1: ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310 react-native-uitextview@1.4.0: 62de26caadfc39d1bdff204cdc03a705c0cd343999825e06d8c0c120de06cc99 react-native-view-shot@4.0.3: a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268 react-native@0.81.5: 2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194 @@ -642,10 +642,10 @@ importers: version: 6.8.0(patch_hash=0979d6fe1e2fa43b2784cc0b5e0ff0117d53b53423e85ee5855eefdc41a00108)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-progress: specifier: bluesky-social/react-native-progress - version: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) react-native-qrcode-styled: specifier: ^0.3.3 - version: 0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: specifier: 3.19.1 version: 3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -657,7 +657,7 @@ importers: version: 4.24.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-svg: specifier: 15.12.1 - version: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-uitextview: specifier: ^1.4.0 version: 1.4.0(patch_hash=62de26caadfc39d1bdff204cdc03a705c0cd343999825e06d8c0c120de06cc99)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -696,7 +696,7 @@ importers: version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) sonner-native: specifier: 0.21.0 - version: 0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(4e42d15c94c2c56af69d343a007eb893) + version: 0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(0ed429160839218d22ac63d892397351) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -17082,18 +17082,18 @@ snapshots: react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) - react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + react-native-progress@https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): dependencies: prop-types: 15.8.1 - react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: qrcode: 1.5.4 react: 19.1.0 react-fast-compare: 3.2.2 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) - react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated@3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: @@ -17127,7 +17127,7 @@ snapshots: react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) warn-once: 0.1.1 - react-native-svg@15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: css-select: 5.2.2 css-tree: 1.1.3 @@ -17677,7 +17677,7 @@ snapshots: uuid: 8.3.2 websocket-driver: 0.7.4 - sonner-native@0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(4e42d15c94c2c56af69d343a007eb893): + sonner-native@0.21.0(patch_hash=4bcd0da0fec32e943460e6e788a9e4adf601b507d2dfd0c8246a6c8c74d8f638)(0ed429160839218d22ac63d892397351): dependencies: react: 19.1.0 react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) @@ -17685,7 +17685,7 @@ snapshots: react-native-reanimated: 3.19.1(patch_hash=97a1967f37a4213fbab59e1fd59a1bf859b5efc79af5e1b528a47fe488e6c5d6)(@babel/core@7.29.0)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-screens: 4.24.0(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-svg: 15.12.1(patch_hash=31401fa6ff01498773afb51a7b066821c471eb3e71b0764a10017938beed49e9)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 76fa7187b2..5207a3a123 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1003,14 +1003,15 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { }) if (IS_WEB) { - const referrerInfo = Referrer.getReferrerInfo() - if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { - ax.metric('deepLink:referrerReceived', { - to: window.location.href, - referrer: referrerInfo?.referrer, - hostname: referrerInfo?.hostname, - }) - } + void Referrer.getReferrerInfo().then(referrerInfo => { + if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { + ax.metric('deepLink:referrerReceived', { + to: window.location.href, + referrer: referrerInfo?.referrer, + hostname: referrerInfo?.hostname, + }) + } + }) } // temp, just testing diff --git a/src/components/ContextMenu/index.web.tsx b/src/components/ContextMenu/index.web.tsx index 3dfc39ffaa..2d45f79db4 100644 --- a/src/components/ContextMenu/index.web.tsx +++ b/src/components/ContextMenu/index.web.tsx @@ -2,8 +2,9 @@ import {type StyleProp, type ViewStyle} from 'react-native' import {atoms as a, useTheme} from '#/alf' import * as Menu from '#/components/Menu' +import {type TriggerProps as MenuTriggerProps} from '#/components/Menu/types' import {Text} from '#/components/Typography' -import {type AuxiliaryViewProps} from './types' +import {type AuxiliaryViewProps, type TriggerProps} from './types' export { ContainerItem, @@ -16,7 +17,6 @@ export { ItemText, LabelText, Root, - Trigger, useMenuContext as useContextMenuContext, useMenuControl as useContextMenuControl, } from '#/components/Menu' @@ -30,14 +30,37 @@ export function AuxiliaryView({}: AuxiliaryViewProps) { return null } +/* + * On web the context menu is just a Menu; contentLabel, onTap, style, and + * swipeGesture only apply to the native press-and-hold presentation. + */ +export function Trigger({children, label, hint, role}: TriggerProps) { + return ( + + {/* + * Menu supplies the same web-arm child props shape as ContextMenu's + * TriggerChildProps; only the native arms of the two unions differ, + * and those never occur here. + */} + {children as unknown as MenuTriggerProps['children']} + + ) +} + export function Outer({ children, label, + align: _align, style, onCloseAutoFocus, }: { children: React.ReactNode label?: string + /** + * Native positions the menu against the message bubble explicitly; the web + * dropdown is anchored by radix, so this is accepted only for parity. + */ + align?: 'left' | 'right' style?: StyleProp onCloseAutoFocus?: (event: Event) => void }) { diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 37c893be1a..3bb9e478a7 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -12,6 +12,7 @@ import { type GestureResponderEvent, type LayoutChangeEvent, Pressable, + type ScrollView, type StyleProp, View, type ViewStyle, @@ -24,6 +25,7 @@ import {RemoveScrollBar} from 'react-remove-scroll-bar' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' +import {type ListMethods} from '#/view/com/util/List' import {atoms as a, flatten, useBreakpoints, useTheme, web} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {Context} from '#/components/Dialog/context' @@ -224,11 +226,21 @@ export function Inner({ ) } -export const ScrollableInner = Inner +/* + * There is no inner ScrollView on web - the ref is accepted for parity with + * the native variant and never attached, so native-only scrolling code in + * shared callers stays a no-op here. + */ +export function ScrollableInner({ + ref: _ref, + ...props +}: DialogInnerProps & {ref?: React.Ref}) { + return +} export const InnerFlatList = forwardRef< - FlatList, - FlatListProps & {label: string} & { + ListMethods, + FlatListProps & {label?: string} & { webInnerStyle?: StyleProp webInnerContentContainerStyle?: StyleProp footer?: React.ReactNode @@ -247,7 +259,11 @@ export const InnerFlatList = forwardRef< const {gtMobile} = useBreakpoints() return ( } style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, style]} {...props} /> @@ -321,7 +343,11 @@ export function Close() { ) } -export function Handle() { +/* + * The drag handle only exists on the native bottom sheet; props are accepted + * for parity with the native variant. + */ +export function Handle(_props: {difference?: boolean; fill?: string}) { return null } diff --git a/src/components/DraggableList/index.web.tsx b/src/components/DraggableList/index.web.tsx index 237a11be35..7dd02cb76c 100644 --- a/src/components/DraggableList/index.web.tsx +++ b/src/components/DraggableList/index.web.tsx @@ -1,5 +1,7 @@ import {useState} from 'react' import {View} from 'react-native' +import type Animated from 'react-native-reanimated' +import {type AnimatedRef, type SharedValue} from 'react-native-reanimated' import {useTheme} from '#/alf' import {DotGrid2x3_Stroke2_Corner0_Rounded as GripIcon} from '#/components/icons/DotGrid' @@ -18,6 +20,10 @@ interface SortableListProps { onDragEnd?: () => void /** Fixed row height used for position math. */ itemHeight: number + /** Ref to the parent Animated.ScrollView for auto-scroll. Ignored on web. */ + scrollRef?: AnimatedRef + /** Scroll offset shared value from useScrollViewOffset. Ignored on web. */ + scrollOffset?: SharedValue } export function SortableList({ diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index d89c5a2e71..fed19c81af 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -32,6 +32,8 @@ import { import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' +export {type DialogControlProps as MenuControlProps} from '#/components/Dialog' + export {useMenuContext} export function useMenuControl(): Dialog.DialogControlProps { diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx index 8664aae142..10e8c73236 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.native.tsx @@ -1,3 +1,10 @@ -export function VideoEmbedInnerWeb() { +import {type VideoEmbedInnerWebProps} from './VideoEmbedInnerWeb.shared' + +export { + HLSUnsupportedError, + VideoNotFoundError, +} from './VideoEmbedInnerWeb.shared' + +export function VideoEmbedInnerWeb(_props: VideoEmbedInnerWebProps): never { throw new Error('VideoEmbedInnerWeb may not be used on native.') } diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts new file mode 100644 index 0000000000..06abf40408 --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.shared.ts @@ -0,0 +1,21 @@ +import {type AppBskyEmbedVideo} from '@atproto/api' + +export type VideoEmbedInnerWebProps = { + embed: AppBskyEmbedVideo.View + active: boolean + setActive: () => void + onScreen: boolean + lastKnownTime: React.RefObject +} + +export class HLSUnsupportedError extends Error { + constructor() { + super('HLS is not supported') + } +} + +export class VideoNotFoundError extends Error { + constructor() { + super('Video not found') + } +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index b97317dfbf..72d9f3a8ba 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,6 +1,5 @@ import {useCallback, useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' -import {type AppBskyEmbedVideo} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import type * as HlsTypes from 'hls.js' @@ -10,21 +9,25 @@ import {atoms as a} from '#/alf' import {AltBadgeWithDialog} from '#/components/AltBadgeWithDialog' import {useFullscreen} from '#/components/hooks/useFullscreen' import * as BandwidthEstimate from './bandwidth-estimate' +import { + HLSUnsupportedError, + type VideoEmbedInnerWebProps, + VideoNotFoundError, +} from './VideoEmbedInnerWeb.shared' import {Controls} from './web-controls/VideoControls' +export { + HLSUnsupportedError, + VideoNotFoundError, +} from './VideoEmbedInnerWeb.shared' + export function VideoEmbedInnerWeb({ embed, active, setActive, onScreen, lastKnownTime, -}: { - embed: AppBskyEmbedVideo.View - active: boolean - setActive: () => void - onScreen: boolean - lastKnownTime: React.RefObject -}) { +}: VideoEmbedInnerWebProps) { const containerRef = useRef(null) const videoRef = useRef(null) const [focused, setFocused] = useState(false) @@ -104,12 +107,6 @@ export function VideoEmbedInnerWeb({ ) } -export class HLSUnsupportedError extends Error { - constructor() { - super('HLS is not supported') - } -} - // Bluesky serves HLS as MPEG-TS with H.264 + AAC. `Hls.isSupported()` is loose // (true if MSE supports *any* of {H.264, AV1, VP9} OR *any* of {AAC, FLAC}), // so on Linux boxes missing H.264 (e.g. no ubuntu-restricted-extras, sandboxed @@ -138,12 +135,6 @@ function canPlayBskyVideoCodecs(): boolean { ) } -export class VideoNotFoundError extends Error { - constructor() { - super('Video not found') - } -} - type CachedPromise = Promise & {value: undefined | T} const promiseForHls = import( // @ts-ignore diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx index e2e24ed367..9535d0fc1f 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.native.tsx @@ -1,3 +1,5 @@ -export function Controls() { +import {type ControlsProps} from './VideoControls.shared' + +export function Controls(_props: ControlsProps): never { throw new Error('VideoWebControls may not be used on native.') } diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.shared.ts b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.shared.ts new file mode 100644 index 0000000000..173aa9f1fb --- /dev/null +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.shared.ts @@ -0,0 +1,17 @@ +import type Hls from 'hls.js' + +export type ControlsProps = { + videoRef: React.RefObject + hlsRef: React.RefObject + active: boolean + setActive: () => void + focused: boolean + setFocused: (focused: boolean) => void + onScreen: boolean + fullscreenRef: React.RefObject + hlsLoading: boolean + hasSubtitleTrack: boolean + isGif: boolean + altText?: string + updateCuePositions: (controlsVisible?: boolean) => void +} diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx index 2e5822589e..57f52e2f22 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx @@ -3,7 +3,6 @@ import {Pressable, View} from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import type Hls from 'hls.js' import {clamp} from '#/lib/numbers' import { @@ -33,6 +32,7 @@ import {TimeIndicator} from '../TimeIndicator' import {ControlButton} from './ControlButton' import {Scrubber} from './Scrubber' import {formatTime, useVideoElement} from './utils' +import {type ControlsProps} from './VideoControls.shared' import {VolumeControl} from './VolumeControl' export function Controls({ @@ -49,21 +49,7 @@ export function Controls({ isGif, altText, updateCuePositions, -}: { - videoRef: React.RefObject - hlsRef: React.RefObject - active: boolean - setActive: () => void - focused: boolean - setFocused: (focused: boolean) => void - onScreen: boolean - fullscreenRef: React.RefObject - hlsLoading: boolean - hasSubtitleTrack: boolean - isGif: boolean - altText?: string - updateCuePositions: (controlsVisible?: boolean) => void -}) { +}: ControlsProps) { const { play, pause, diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index 9e02d5a3cc..c087f5fa46 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -23,7 +23,7 @@ import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {Default as ProfileCard} from '#/components/ProfileCard' import {IS_NATIVE, IS_WEB} from '#/env' -function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { +function keyExtractor(item: AppBskyActorDefs.ProfileView, index: number) { return `${item.did}-${index}` } @@ -87,7 +87,7 @@ export const ProfilesList = forwardRef( const renderItem = ({ item, index, - }: ListRenderItemInfo) => { + }: ListRenderItemInfo) => { return ( | React.ReactElement } -function keyExtractor(item: AppBskyGraphDefs.StarterPackView) { +function keyExtractor(item: AppBskyGraphDefs.StarterPackViewBasic) { return item.uri } @@ -138,13 +138,16 @@ export function ProfileStarterPacks({ useEffect(() => { if (IS_IOS && enabled && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) + const nativeTag = findListNativeTag(scrollElRef.current) setScrollViewTag(nativeTag) } }, [enabled, scrollElRef, setScrollViewTag]) const renderItem = useCallback( - ({item, index}: ListRenderItemInfo) => { + ({ + item, + index, + }: ListRenderItemInfo) => { return ( void - close: () => void - } + control: WelcomeModalControl } export function WelcomeModal({control}: WelcomeModalProps) { diff --git a/src/components/contacts/FindContactsFlow.web.tsx b/src/components/contacts/FindContactsFlow.web.tsx index 9def7df16b..f1c720087b 100644 --- a/src/components/contacts/FindContactsFlow.web.tsx +++ b/src/components/contacts/FindContactsFlow.web.tsx @@ -1,3 +1,11 @@ -export function FindContactsFlow() { +import {type Action, type State} from './state' + +export function FindContactsFlow(_props: { + state: State + dispatch: React.ActionDispatch<[Action]> + onBack?: () => void + onCancel: () => void + context: 'Onboarding' | 'Standalone' +}): never { throw new Error('FindContactsFlow is not available on web') } diff --git a/src/components/hooks/useWelcomeModal.native.ts b/src/components/hooks/useWelcomeModal.native.ts index f5bc1aa4ed..bdd0805d32 100644 --- a/src/components/hooks/useWelcomeModal.native.ts +++ b/src/components/hooks/useWelcomeModal.native.ts @@ -1,3 +1,5 @@ -export function useWelcomeModal() { +import {type WelcomeModalControl} from './useWelcomeModal.shared' + +export function useWelcomeModal(): WelcomeModalControl { throw new Error('useWelcomeModal is web only') } diff --git a/src/components/hooks/useWelcomeModal.shared.ts b/src/components/hooks/useWelcomeModal.shared.ts new file mode 100644 index 0000000000..1151475e28 --- /dev/null +++ b/src/components/hooks/useWelcomeModal.shared.ts @@ -0,0 +1,5 @@ +export type WelcomeModalControl = { + isOpen: boolean + open: () => void + close: () => void +} diff --git a/src/components/hooks/useWelcomeModal.ts b/src/components/hooks/useWelcomeModal.ts index d0a55a0c18..3e33084b96 100644 --- a/src/components/hooks/useWelcomeModal.ts +++ b/src/components/hooks/useWelcomeModal.ts @@ -2,8 +2,9 @@ import {useEffect, useState} from 'react' import {useSession} from '#/state/session' import {IS_WEB} from '#/env' +import {type WelcomeModalControl} from './useWelcomeModal.shared' -export function useWelcomeModal() { +export function useWelcomeModal(): WelcomeModalControl { const {hasSession} = useSession() const [isOpen, setIsOpen] = useState(false) diff --git a/src/lib/custom-animations/GestureActionView.shared.ts b/src/lib/custom-animations/GestureActionView.shared.ts new file mode 100644 index 0000000000..c987d61ffb --- /dev/null +++ b/src/lib/custom-animations/GestureActionView.shared.ts @@ -0,0 +1,15 @@ +import {type ColorValue} from 'react-native' + +export interface GestureAction { + color: ColorValue + action: () => void + threshold: number + icon: React.ElementType +} + +export interface GestureActions { + leftFirst?: GestureAction + leftSecond?: GestureAction + rightFirst?: GestureAction + rightSecond?: GestureAction +} diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx index cadc9389da..71b7864df2 100644 --- a/src/lib/custom-animations/GestureActionView.tsx +++ b/src/lib/custom-animations/GestureActionView.tsx @@ -16,20 +16,7 @@ import Animated, { } from 'react-native-reanimated' import {useHaptics} from '#/lib/haptics' - -interface GestureAction { - color: ColorValue - action: () => void - threshold: number - icon: React.ElementType -} - -interface GestureActions { - leftFirst?: GestureAction - leftSecond?: GestureAction - rightFirst?: GestureAction - rightSecond?: GestureAction -} +import {type GestureActions} from './GestureActionView.shared' const MAX_WIDTH = Dimensions.get('screen').width const ICON_SIZE = 32 diff --git a/src/lib/custom-animations/GestureActionView.web.tsx b/src/lib/custom-animations/GestureActionView.web.tsx index 9fed076a03..b27a90614e 100644 --- a/src/lib/custom-animations/GestureActionView.web.tsx +++ b/src/lib/custom-animations/GestureActionView.web.tsx @@ -1,3 +1,13 @@ -export function GestureActionView({children}: {children: React.ReactNode}) { +import {type GestureActions} from './GestureActionView.shared' + +/* + * Swipe gestures only exist on native; children render as-is on web. + */ +export function GestureActionView({ + children, +}: { + children: React.ReactNode + actions: GestureActions +}) { return children } diff --git a/src/lib/hooks/useIntentHandler.ts b/src/lib/hooks/useIntentHandler.ts index 5a9d0b39fe..eaa6482830 100644 --- a/src/lib/hooks/useIntentHandler.ts +++ b/src/lib/hooks/useIntentHandler.ts @@ -40,7 +40,7 @@ export function useIntentHandler() { await WebBrowser.dismissBrowser().catch(() => {}) } - const referrerInfo = Referrer.getReferrerInfo() + const referrerInfo = await Referrer.getReferrerInfo() if (referrerInfo && referrerInfo.hostname !== 'bsky.app') { ax.metric('deepLink:referrerReceived', { to: url, diff --git a/src/lib/hooks/useNavigationTabState.web.ts b/src/lib/hooks/useNavigationTabState.web.ts index 03dcbbb038..6af96fed58 100644 --- a/src/lib/hooks/useNavigationTabState.web.ts +++ b/src/lib/hooks/useNavigationTabState.web.ts @@ -8,6 +8,8 @@ export function useNavigationTabState() { return { isAtHome: currentRoute === 'Home', isAtSearch: currentRoute === 'Search', + isAtFeeds: currentRoute === 'Feeds', + isAtBookmarks: currentRoute === 'Bookmarks', isAtNotifications: currentRoute === 'Notifications', isAtMyProfile: currentRoute === 'MyProfile', isAtMessages: currentRoute === 'Messages', diff --git a/src/lib/hooks/useOTAUpdates.web.ts b/src/lib/hooks/useOTAUpdates.web.ts index 2783a04dd1..30cb2c6dba 100644 --- a/src/lib/hooks/useOTAUpdates.web.ts +++ b/src/lib/hooks/useOTAUpdates.web.ts @@ -1,7 +1,7 @@ export function useOTAUpdates() {} export function useApplyPullRequestOTAUpdate() { return { - tryApplyUpdate: () => {}, + tryApplyUpdate: async (_channel: string) => {}, revertToEmbedded: () => {}, isCurrentlyRunningPullRequestDeployment: false, currentChannel: 'web-build', diff --git a/src/lib/hotkeys/index.native.tsx b/src/lib/hotkeys/index.native.tsx index a0fe0e105a..58bfb1fef8 100644 --- a/src/lib/hotkeys/index.native.tsx +++ b/src/lib/hotkeys/index.native.tsx @@ -4,7 +4,7 @@ export function Provider({children}: {children: React.ReactNode}) { return children } -const noop = () => {} +const noop = (_scope: string) => {} export function useHotkeysContext() { return useMemo( diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts index 94732687ef..690dc32023 100644 --- a/src/lib/media/manip.web.ts +++ b/src/lib/media/manip.web.ts @@ -168,10 +168,14 @@ function createResizedImage( export async function saveBytesToDisk( filename: string, - bytes: Uint8Array, + bytes: Uint8Array, type: string, ) { - const blob = new Blob([bytes], {type}) + /* + * Bytes handed to us are never SharedArrayBuffer-backed, but the broader + * Uint8Array parameter type matches the native variant. + */ + const blob = new Blob([bytes as Uint8Array], {type}) const url = URL.createObjectURL(blob) downloadUrl(url, filename) // Firefox requires a small delay diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index 060169ff89..1cd99a05c7 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -1,11 +1,13 @@ +import {type ImagePickerOptions} from 'expo-image-picker' import {type OpenCropperOptions} from '@bsky.app/expo-image-crop-tool' import {type PickerImage} from './picker.shared' -import {type CameraOpts} from './types' export {openPicker, openUnifiedPicker} from './picker.shared' -export async function openCamera(_opts: CameraOpts): Promise { +export async function openCamera( + _opts: ImagePickerOptions, +): Promise { throw new Error('openCamera is not supported on web') } diff --git a/src/lib/media/types.ts b/src/lib/media/types.ts index c083093ac7..c4d53ffedd 100644 --- a/src/lib/media/types.ts +++ b/src/lib/media/types.ts @@ -8,10 +8,3 @@ export interface PickerOpts { multiple?: boolean maxFiles?: number } - -export interface CameraOpts { - width: number - height: number - freeStyleCropEnabled?: boolean - cropperCircleOverlay?: boolean -} diff --git a/src/platform/misc.android-check.d.ts b/src/platform/misc.android-check.d.ts new file mode 100644 index 0000000000..9c6361b5fb --- /dev/null +++ b/src/platform/misc.android-check.d.ts @@ -0,0 +1,13 @@ +/* + * Used ONLY by the Android typecheck pass (tsconfig.check.android.json) - it + * is excluded from the other passes and has no runtime effect. + * + * react-native-device-attest's Android variant implements only the Play + * Integrity statics, so the iOS-only getDeviceCheckToken (whose app call + * site is IS_IOS-guarded) fails to resolve under .android module suffixes. + * Pin the module to its platform-neutral base class, which declares the + * full API surface. + */ +declare module 'react-native-device-attest' { + export {DeviceAttestBase as default} from 'react-native-device-attest/build/DeviceAttestBase' +} diff --git a/src/platform/misc.web-check.d.ts b/src/platform/misc.web-check.d.ts new file mode 100644 index 0000000000..a592015d56 --- /dev/null +++ b/src/platform/misc.web-check.d.ts @@ -0,0 +1,102 @@ +/* + * Used ONLY by the web typecheck pass (tsconfig.check.web.json) - it is + * excluded from the other passes and has no runtime effect. + * + * Some packages publish .web.d.ts declarations that diverge from the API + * the app is written against (or, worse, raw TypeScript sources that break + * under `moduleSuffixes: [".web", ""]`), so this pins them to their native + * declarations. react-native-svg has the same problem, handled by deleting + * its divergent web declarations in patches/react-native-svg@15.12.1.patch + * so resolution falls through to the native ones. + */ + +/* + * Side-effect CSS imports in .web files are handled by the bundler. + */ +declare module '*.css' + +/* + * expo-file-system's declarations build File/Directory on top of + * `./ExpoFileSystem`, which remaps to a web shim whose classes are empty. + * The fully-typed base classes live in ExpoFileSystem.types (no .web + * sibling), so mirror the FileSystem.d.ts wrapper classes on top of those. + */ +declare module 'expo-file-system' { + import { + Directory as ExpoFileSystemDirectory, + File as ExpoFileSystemFile, + } from 'expo-file-system/build/ExpoFileSystem.types' + export { + type DirectoryCreateOptions, + type DirectoryInfo, + type DownloadOptions, + EncodingType, + type FileCreateOptions, + FileHandle, + type FileInfo, + type FileWriteOptions, + type InfoOptions, + type PathInfo, + } from 'expo-file-system/build/ExpoFileSystem.types' + import {type PathInfo as ExpoPathInfo} from 'expo-file-system/build/ExpoFileSystem.types' + import {PathUtilities} from 'expo-file-system/build/pathUtilities' + + export class Paths extends PathUtilities { + static get cache(): Directory + static get bundle(): Directory + static get document(): Directory + static get appleSharedContainers(): Record + static get totalDiskSpace(): number + static get availableDiskSpace(): number + static info(...uris: string[]): ExpoPathInfo + } + + export class File extends ExpoFileSystemFile implements Blob { + constructor(...uris: (string | File | Directory)[]) + get parentDirectory(): Directory + get extension(): string + get name(): string + readableStream(): ReadableStream> + writableStream(): WritableStream> + arrayBuffer(): Promise + stream(): ReadableStream> + slice(start?: number, end?: number, contentType?: string): Blob + } + + export class Directory extends ExpoFileSystemDirectory { + constructor(...uris: (string | File | Directory)[]) + get parentDirectory(): Directory + list(): (Directory | File)[] + get name(): string + createFile(name: string, mimeType: string | null): File + createDirectory(name: string): Directory + } +} + +/* + * expo-image-manipulator's web declarations export the module class where + * the native ones export an instance, hiding instance methods such as + * `manipulate` from `typeof ImageManipulator`. Mirror the native surface. + */ +declare module 'expo-image-manipulator' { + import {type ImageManipulator as ImageManipulatorModule} from 'expo-image-manipulator/build/ImageManipulator.types' + export const ImageManipulator: ImageManipulatorModule + export { + manipulateAsync, + useImageManipulator, + } from 'expo-image-manipulator/build/ImageManipulator' + export { + type Action, + type ActionCrop, + type ActionExtent, + type ActionFlip, + type ActionResize, + type ActionRotate, + FlipType, + type ImageResult, + SaveFormat, + type SaveOptions, + } from 'expo-image-manipulator/build/ImageManipulator.types' + export {type ImageManipulatorContext} from 'expo-image-manipulator/build/ImageManipulatorContext' + export {type ImageRef} from 'expo-image-manipulator/build/ImageRef' +} diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 26e225cbd2..b7eb113a49 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -1,6 +1,5 @@ import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {View} from 'react-native' -import {useAnimatedRef} from 'react-native-reanimated' import {type ChatBskyActorGetStatus, type ChatBskyConvoDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import { @@ -275,7 +274,7 @@ export function ChatList({ const t = useTheme() const {t: l} = useLingui() const aa = useAgeAssurance() - const scrollElRef: ListRef = useAnimatedRef() + const scrollElRef: ListRef = useRef(null) const {isWithinSplitView} = useIsWithinSplitView() const openChatControl = useCallback(() => { diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index a3468878a5..794740d0b8 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -17,7 +17,6 @@ import Animated, { runOnJS, type ScrollEvent, type SharedValue, - useAnimatedRef, useAnimatedStyle, useDerivedValue, useSharedValue, @@ -154,7 +153,7 @@ export function MessagesList({ const t = useTheme() const textInputId = 'chat-input-' + useId() - const flatListRef = useAnimatedRef() + const flatListRef = useRef(null) const [expandedGroups, setExpandedGroups] = useState>( () => new Set(), diff --git a/src/screens/Onboarding/StepFindContacts/index.web.tsx b/src/screens/Onboarding/StepFindContacts/index.web.tsx index dc9b4d0ef7..f062156839 100644 --- a/src/screens/Onboarding/StepFindContacts/index.web.tsx +++ b/src/screens/Onboarding/StepFindContacts/index.web.tsx @@ -1,3 +1,8 @@ -export function StepFindContacts() { +import {type Action, type State} from '#/components/contacts/state' + +export function StepFindContacts(_props: { + flowState: State + flowDispatch: React.ActionDispatch<[Action]> +}): never { throw new Error('StepFindContacts is not available on web') } diff --git a/src/screens/Onboarding/StepFindContactsIntro/index.web.tsx b/src/screens/Onboarding/StepFindContactsIntro/index.web.tsx index bb58f202b9..64194890f4 100644 --- a/src/screens/Onboarding/StepFindContactsIntro/index.web.tsx +++ b/src/screens/Onboarding/StepFindContactsIntro/index.web.tsx @@ -1,3 +1,3 @@ -export function StepFindContactsIntro() { +export function StepFindContactsIntro(): never { throw new Error('StepFindContactsIntro is not available on web') } diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx index 479657bb3a..5b0da24ccf 100644 --- a/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.shared.tsx @@ -4,6 +4,16 @@ import {useLingui} from '@lingui/react' import {atoms as a, useTheme} from '#/alf' +export type ValuePropositionPagerProps = { + step: 0 | 1 | 2 + /** + * Only the native pager changes pages itself (by swiping); the web pager + * is driven entirely by the step prop. + */ + setStep: (step: 0 | 1 | 2) => void + avatarUri?: string +} + export function useValuePropText(step: 0 | 1 | 2) { const {_} = useLingui() diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx index 1641547bb2..f617c01888 100644 --- a/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.tsx @@ -8,17 +8,17 @@ import {useLingui} from '@lingui/react' import {atoms as a, tokens, useTheme} from '#/alf' import {Text} from '#/components/Typography' import {PROP_1, PROP_2, PROP_3} from './images' -import {Dot, useValuePropText} from './ValuePropositionPager.shared' +import { + Dot, + useValuePropText, + type ValuePropositionPagerProps, +} from './ValuePropositionPager.shared' export function ValuePropositionPager({ step, setStep, avatarUri, -}: { - step: 0 | 1 | 2 - setStep: (step: 0 | 1 | 2) => void - avatarUri?: string -}) { +}: ValuePropositionPagerProps) { const t = useTheme() const [activePage, setActivePage] = useState(step) const ref = useRef(null) diff --git a/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx index a7c685506b..ba71a5681f 100644 --- a/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx +++ b/src/screens/Onboarding/StepFinished/ValuePropositionPager.web.tsx @@ -6,15 +6,16 @@ import {useLingui} from '@lingui/react' import {atoms as a, useTheme} from '#/alf' import {Text} from '#/components/Typography' import {PROP_1, PROP_2, PROP_3} from './images' -import {Dot, useValuePropText} from './ValuePropositionPager.shared' +import { + Dot, + useValuePropText, + type ValuePropositionPagerProps, +} from './ValuePropositionPager.shared' export function ValuePropositionPager({ step, avatarUri, -}: { - step: 0 | 1 | 2 - avatarUri?: string -}) { +}: ValuePropositionPagerProps) { const t = useTheme() const {_} = useLingui() diff --git a/src/screens/Profile/KnownFollowers.tsx b/src/screens/Profile/KnownFollowers.tsx index 32a5076829..09c1034b07 100644 --- a/src/screens/Profile/KnownFollowers.tsx +++ b/src/screens/Profile/KnownFollowers.tsx @@ -34,7 +34,7 @@ function renderItem({ ) } -function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { +function keyExtractor(item: AppBskyActorDefs.ProfileView) { return item.did } diff --git a/src/screens/Profile/Sections/Feed.tsx b/src/screens/Profile/Sections/Feed.tsx index 37ced06bbc..1f6863ff36 100644 --- a/src/screens/Profile/Sections/Feed.tsx +++ b/src/screens/Profile/Sections/Feed.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useImperativeHandle, useState} from 'react' -import {findNodeHandle, View} from 'react-native' +import {View} from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -17,6 +17,7 @@ import { type EmptyStateButtonProps, } from '#/view/com/util/EmptyState' import {type ListRef} from '#/view/com/util/List' +import {findListNativeTag} from '#/view/com/util/listNativeTag' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {atoms as a, ios, useTheme} from '#/alf' import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig' @@ -87,7 +88,7 @@ export function ProfileFeedSection({ useEffect(() => { if (IS_IOS && isFocused && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) + const nativeTag = findListNativeTag(scrollElRef.current) setScrollViewTag(nativeTag) } }, [isFocused, scrollElRef, setScrollViewTag]) diff --git a/src/screens/Profile/Sections/Labels.tsx b/src/screens/Profile/Sections/Labels.tsx index e4af8c4a86..8c70d40392 100644 --- a/src/screens/Profile/Sections/Labels.tsx +++ b/src/screens/Profile/Sections/Labels.tsx @@ -1,5 +1,5 @@ import {useCallback, useEffect, useImperativeHandle, useMemo} from 'react' -import {findNodeHandle, type ListRenderItemInfo, View} from 'react-native' +import {type ListRenderItemInfo, View} from 'react-native' import { type AppBskyLabelerDefs, type InterpretedLabelValueDefinition, @@ -12,6 +12,7 @@ import {Trans} from '@lingui/react/macro' import {isLabelerSubscribed, lookupLabelValueDefinition} from '#/lib/moderation' import {List, type ListRef} from '#/view/com/util/List' +import {findListNativeTag} from '#/view/com/util/listNativeTag' import {atoms as a, ios, tokens, useTheme} from '#/alf' import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' @@ -61,7 +62,7 @@ export function ProfileLabelsSection({ useEffect(() => { if (IS_IOS && isFocused && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) + const nativeTag = findListNativeTag(scrollElRef.current) setScrollViewTag(nativeTag) } }, [isFocused, scrollElRef, setScrollViewTag]) diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 0afc72e1a6..95f58645ad 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -1169,7 +1169,7 @@ export function Explore({ ) } -function keyExtractor(item: FeedPreviewItem) { +function keyExtractor(item: ExploreScreenItems) { return item.key } diff --git a/src/screens/Settings/AppIconSettings/SettingsListItem.web.tsx b/src/screens/Settings/AppIconSettings/SettingsListItem.web.tsx index c7707d23fd..4c2585bccc 100644 --- a/src/screens/Settings/AppIconSettings/SettingsListItem.web.tsx +++ b/src/screens/Settings/AppIconSettings/SettingsListItem.web.tsx @@ -1 +1,3 @@ -export function SettingsListItem() {} +export function SettingsListItem() { + return null +} diff --git a/src/screens/Settings/AppIconSettings/index.web.tsx b/src/screens/Settings/AppIconSettings/index.web.tsx index d8c4d9bea7..cff23d49c9 100644 --- a/src/screens/Settings/AppIconSettings/index.web.tsx +++ b/src/screens/Settings/AppIconSettings/index.web.tsx @@ -1,3 +1,3 @@ -export function AppIconSettingsScreen() { +export function AppIconSettingsScreen(): never { throw new Error('Not supported on web') } diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.shared.ts b/src/screens/Signup/StepCaptcha/CaptchaWebView.shared.ts new file mode 100644 index 0000000000..7a4c02b45d --- /dev/null +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.shared.ts @@ -0,0 +1,10 @@ +import {type SignupState} from '#/screens/Signup/state' + +export type CaptchaWebViewProps = { + url: string + stateParam: string + state?: SignupState + onComplete: () => void + onSuccess: (code: string) => void + onError: (error: unknown) => void +} diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx index fa7b3064ee..bf0b9a2483 100644 --- a/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.tsx @@ -2,7 +2,7 @@ import {useEffect, useMemo, useRef} from 'react' import {WebView, type WebViewNavigation} from 'react-native-webview' import {type ShouldStartLoadRequest} from 'react-native-webview/lib/WebViewTypes' -import {type SignupState} from '#/screens/Signup/state' +import {type CaptchaWebViewProps} from './CaptchaWebView.shared' const ALLOWED_HOSTS = [ 'bsky.social', @@ -24,14 +24,7 @@ export function CaptchaWebView({ onComplete, onSuccess, onError, -}: { - url: string - stateParam: string - state?: SignupState - onComplete: () => void - onSuccess: (code: string) => void - onError: (error: unknown) => void -}) { +}: CaptchaWebViewProps) { const startedAt = useRef(Date.now()) const successTo = useRef(undefined) diff --git a/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx index 3518400779..b8c3832607 100644 --- a/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx +++ b/src/screens/Signup/StepCaptcha/CaptchaWebView.web.tsx @@ -1,6 +1,8 @@ import {useCallback, useEffect} from 'react' import {StyleSheet} from 'react-native' +import {type CaptchaWebViewProps} from './CaptchaWebView.shared' + // @ts-ignore web only, we will always redirect to the app on web (CORS) const REDIRECT_HOST = new URL(window.location.href).host @@ -9,12 +11,7 @@ export function CaptchaWebView({ stateParam, onSuccess, onError, -}: { - url: string - stateParam: string - onSuccess: (code: string) => void - onError: (error: unknown) => void -}) { +}: CaptchaWebViewProps) { useEffect(() => { const timeout = setTimeout(() => { onError({ diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx index 61a4f45ef4..4aef058b27 100644 --- a/src/screens/StarterPack/Wizard/StepProfiles.tsx +++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx @@ -1,7 +1,7 @@ import {useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' -import {type AppBskyActorDefs, type ModerationOpts} from '@atproto/api' +import {type ModerationOpts} from '@atproto/api' import {Trans} from '@lingui/react/macro' import {useA11y} from '#/state/a11y' @@ -18,7 +18,7 @@ import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' import type * as bsky from '#/types/bsky' -function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic) { +function keyExtractor(item: bsky.profile.AnyProfileView) { return item?.did ?? '' } diff --git a/src/view/com/composer/photos/EditImageDialog.shared.ts b/src/view/com/composer/photos/EditImageDialog.shared.ts new file mode 100644 index 0000000000..8c9af6a098 --- /dev/null +++ b/src/view/com/composer/photos/EditImageDialog.shared.ts @@ -0,0 +1,10 @@ +import {type ComposerImage} from '#/state/gallery' +import type * as Dialog from '#/components/Dialog' + +export type EditImageDialogProps = { + control: Dialog.DialogOuterProps['control'] + image?: ComposerImage + onChange: (next: ComposerImage) => void + aspectRatio?: number + circularCrop?: boolean +} diff --git a/src/view/com/composer/photos/EditImageDialog.tsx b/src/view/com/composer/photos/EditImageDialog.tsx index 9a0eb90848..d81c0c0b33 100644 --- a/src/view/com/composer/photos/EditImageDialog.tsx +++ b/src/view/com/composer/photos/EditImageDialog.tsx @@ -1,13 +1,6 @@ -import {type ComposerImage} from '#/state/gallery' -import type * as Dialog from '#/components/Dialog' +import {type EditImageDialogProps} from './EditImageDialog.shared' -export type EditImageDialogProps = { - control: Dialog.DialogOuterProps['control'] - image?: ComposerImage - onChange: (next: ComposerImage) => void - aspectRatio?: number - circularCrop?: boolean -} +export type {EditImageDialogProps} from './EditImageDialog.shared' export const EditImageDialog = ({}: EditImageDialogProps): React.ReactNode => { return null diff --git a/src/view/com/composer/photos/EditImageDialog.web.tsx b/src/view/com/composer/photos/EditImageDialog.web.tsx index 60f4fc1fa2..6a3f65602c 100644 --- a/src/view/com/composer/photos/EditImageDialog.web.tsx +++ b/src/view/com/composer/photos/EditImageDialog.web.tsx @@ -16,7 +16,7 @@ import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' -import {type EditImageDialogProps} from './EditImageDialog' +import {type EditImageDialogProps} from './EditImageDialog.shared' export function EditImageDialog(props: EditImageDialogProps) { return ( diff --git a/src/view/com/composer/photos/OpenCameraBtn.shared.ts b/src/view/com/composer/photos/OpenCameraBtn.shared.ts new file mode 100644 index 0000000000..513db09140 --- /dev/null +++ b/src/view/com/composer/photos/OpenCameraBtn.shared.ts @@ -0,0 +1,6 @@ +import {type ComposerImage} from '#/state/gallery' + +export type OpenCameraBtnProps = { + disabled?: boolean + onAdd: (next: ComposerImage[]) => void +} diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index 320a1b7d03..6365765fc9 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -6,18 +6,14 @@ import {useLingui} from '@lingui/react' import {useCameraPermission} from '#/lib/hooks/usePermissions' import {openCamera} from '#/lib/media/picker' import {logger} from '#/logger' -import {type ComposerImage, createComposerImage} from '#/state/gallery' +import {createComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera' import {IS_NATIVE, IS_WEB_MOBILE} from '#/env' +import {type OpenCameraBtnProps} from './OpenCameraBtn.shared' -type Props = { - disabled?: boolean - onAdd: (next: ComposerImage[]) => void -} - -export function OpenCameraBtn({disabled, onAdd}: Props) { +export function OpenCameraBtn({disabled, onAdd}: OpenCameraBtnProps) { const {_} = useLingui() const {requestCameraAccessIfNeeded} = useCameraPermission() const [mediaPermissionRes, requestMediaPermission] = diff --git a/src/view/com/composer/photos/OpenCameraBtn.web.tsx b/src/view/com/composer/photos/OpenCameraBtn.web.tsx index 226de1f60b..5ca2d54120 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.web.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.web.tsx @@ -1,3 +1,5 @@ -export function OpenCameraBtn() { +import {type OpenCameraBtnProps} from './OpenCameraBtn.shared' + +export function OpenCameraBtn(_props: OpenCameraBtnProps) { return null } diff --git a/src/view/com/composer/videos/SubtitleFilePicker.native.tsx b/src/view/com/composer/videos/SubtitleFilePicker.native.tsx index f2b9a7b04a..b799d2dd84 100644 --- a/src/view/com/composer/videos/SubtitleFilePicker.native.tsx +++ b/src/view/com/composer/videos/SubtitleFilePicker.native.tsx @@ -1,3 +1,5 @@ -export function SubtitleFilePicker() { +import {type SubtitleFilePickerProps} from './SubtitleFilePicker.shared' + +export function SubtitleFilePicker(_props: SubtitleFilePickerProps): never { throw new Error('SubtitleFilePicker is a web-only component') } diff --git a/src/view/com/composer/videos/SubtitleFilePicker.shared.ts b/src/view/com/composer/videos/SubtitleFilePicker.shared.ts new file mode 100644 index 0000000000..b4f6f7591c --- /dev/null +++ b/src/view/com/composer/videos/SubtitleFilePicker.shared.ts @@ -0,0 +1,4 @@ +export type SubtitleFilePickerProps = { + onSelectFile: (file: File) => void + disabled?: boolean +} diff --git a/src/view/com/composer/videos/SubtitleFilePicker.tsx b/src/view/com/composer/videos/SubtitleFilePicker.tsx index 44fa582ddf..23277c8681 100644 --- a/src/view/com/composer/videos/SubtitleFilePicker.tsx +++ b/src/view/com/composer/videos/SubtitleFilePicker.tsx @@ -9,14 +9,12 @@ import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC' import * as Toast from '#/components/Toast' +import {type SubtitleFilePickerProps} from './SubtitleFilePicker.shared' export function SubtitleFilePicker({ onSelectFile, disabled, -}: { - onSelectFile: (file: File) => void - disabled?: boolean -}) { +}: SubtitleFilePickerProps) { const {_} = useLingui() const ref = useRef(null) diff --git a/src/view/com/composer/videos/VideoTranscodeBackdrop.web.tsx b/src/view/com/composer/videos/VideoTranscodeBackdrop.web.tsx index cb8eb52bec..0a5550f265 100644 --- a/src/view/com/composer/videos/VideoTranscodeBackdrop.web.tsx +++ b/src/view/com/composer/videos/VideoTranscodeBackdrop.web.tsx @@ -1,6 +1,8 @@ +import {type QueryClient} from '@tanstack/react-query' + import {atoms as a, flatten} from '#/alf' -export function clearThumbnailCache() { +export function clearThumbnailCache(_queryClient: QueryClient) { // no-op on web } diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 6eedcd8622..effa9ad430 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -6,7 +6,6 @@ import { useState, } from 'react' import { - findNodeHandle, type ListRenderItemInfo, type StyleProp, useWindowDimensions, @@ -26,6 +25,7 @@ import {useSession} from '#/state/session' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListRef} from '#/view/com/util/List' +import {findListNativeTag} from '#/view/com/util/listNativeTag' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {atoms as a, ios, useTheme} from '#/alf' @@ -219,7 +219,7 @@ export function ProfileFeedgens({ useEffect(() => { if (IS_IOS && enabled && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) + const nativeTag = findListNativeTag(scrollElRef.current) setScrollViewTag(nativeTag) } }, [enabled, scrollElRef, setScrollViewTag]) diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index c175793624..afc4c63791 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -6,7 +6,6 @@ import { useState, } from 'react' import { - findNodeHandle, type ListRenderItemInfo, type StyleProp, useWindowDimensions, @@ -26,6 +25,7 @@ import {useSession} from '#/state/session' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, type ListRef} from '#/view/com/util/List' +import {findListNativeTag} from '#/view/com/util/listNativeTag' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {atoms as a, ios, useTheme} from '#/alf' @@ -218,7 +218,7 @@ export function ProfileLists({ useEffect(() => { if (IS_IOS && enabled && scrollElRef.current) { - const nativeTag = findNodeHandle(scrollElRef.current) + const nativeTag = findListNativeTag(scrollElRef.current) setScrollViewTag(nativeTag) } }, [enabled, scrollElRef, setScrollViewTag]) diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index ebe5432a33..cf15639791 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react' import {View} from 'react-native' +import {type SharedValue, useSharedValue} from 'react-native-reanimated' import {flushSync} from 'react-dom' import {s} from '#/lib/styles' @@ -19,7 +20,9 @@ export interface PagerRef { export interface RenderTabBarFnProps { selectedPage: number onSelect?: (index: number) => void - tabBarAnchor?: JSX.Element + tabBarAnchor?: JSX.Element | null | undefined // Ignored on native. + dragProgress: SharedValue // Ignored on web. + dragState: SharedValue<'idle' | 'dragging' | 'settling'> // Ignored on web. } export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element @@ -27,7 +30,17 @@ interface Props { ref?: React.Ref initialPage?: number renderTabBar: RenderTabBarFn + // tab pressed, yet to scroll to page + onTabPressed?: (index: number) => void + // scroll settled onPageSelected?: (index: number) => void + /** + * Never fires on web - pages switch instantly, there is no drag gesture. + */ + onPageScrollStateChanged?: ( + scrollState: 'idle' | 'dragging' | 'settling', + ) => void + testID?: string } export function Pager({ @@ -35,12 +48,22 @@ export function Pager({ children, initialPage = 0, renderTabBar, + onTabPressed, onPageSelected, + onPageScrollStateChanged: _onPageScrollStateChanged, + testID, }: React.PropsWithChildren) { const [selectedPage, setSelectedPage] = useState(initialPage) const scrollYs = useRef>([]) const anchorRef = useRef(null) + /* + * There is no drag gesture on web; these exist to satisfy the shared + * RenderTabBarFnProps contract and never change. + */ + const dragProgress = useSharedValue(selectedPage) + const dragState = useSharedValue<'idle' | 'dragging' | 'settling'>('idle') + useImperativeHandle(ref, () => ({ setPage: (index: number) => { onTabBarSelect(index) @@ -59,6 +82,7 @@ export function Pager({ : -scrollY // If there's no anchor, treat the top of the page as one. const isSticking = anchorTop <= 5 // This would be 0 if browser scrollTo() was reliable. + onTabPressed?.(index) if (isSticking) { scrollYs.current[selectedPage] = window.scrollY } else { @@ -77,15 +101,17 @@ export function Pager({ } } }, - [selectedPage, setSelectedPage, onPageSelected], + [selectedPage, setSelectedPage, onPageSelected, onTabPressed], ) return ( - + {renderTabBar({ selectedPage, tabBarAnchor: , onSelect: e => onTabBarSelect(e), + dragProgress, + dragState, })} {Children.map(children, (child, i) => ( void + setMinimumHeight: (height: number) => void }) => JSX.Element initialPage?: number onPageSelected?: (index: number) => void onCurrentPageSelected?: (index: number) => void + allowHeaderOverScroll?: boolean // Ignored on web. } export const PagerWithHeader = forwardRef( function PageWithHeaderImpl( @@ -127,7 +128,7 @@ let PagerTabBar = ({ renderHeader?: ({ setMinimumHeight, }: { - setMinimumHeight: () => void + setMinimumHeight: (height: number) => void }) => JSX.Element isHeaderReady: boolean onCurrentPageSelected?: (index: number) => void diff --git a/src/view/com/pager/TabBar.web.tsx b/src/view/com/pager/TabBar.web.tsx index bacfaedabd..395e0dce07 100644 --- a/src/view/com/pager/TabBar.web.tsx +++ b/src/view/com/pager/TabBar.web.tsx @@ -1,5 +1,6 @@ import {useCallback, useEffect, useRef} from 'react' import {type ScrollView, StyleSheet, View} from 'react-native' +import {type SharedValue} from 'react-native-reanimated' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {Text} from '#/components/Typography' @@ -15,6 +16,14 @@ export interface TabBarProps { onSelect?: (index: number) => void onPressSelected?: (index: number) => void + + /** + * The drag-following indicator and transparent background only exist in + * the native tab bar - accepted here so shared callers typecheck. + */ + dragProgress?: SharedValue // Ignored on web. + dragState?: SharedValue<'idle' | 'dragging' | 'settling'> // Ignored on web. + transparent?: boolean // Ignored on web. } // How much of the previous/next item we're showing diff --git a/src/view/com/post-thread/PostQuotes.tsx b/src/view/com/post-thread/PostQuotes.tsx index 671334ad3c..b16281f9e0 100644 --- a/src/view/com/post-thread/PostQuotes.tsx +++ b/src/view/com/post-thread/PostQuotes.tsx @@ -17,6 +17,7 @@ import {usePostQuotesQuery} from '#/state/queries/post-quotes' import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {Post} from '#/view/com/post/Post' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' +import * as bsky from '#/types/bsky' import {List} from '../util/List' function renderItem({ @@ -70,7 +71,13 @@ export function PostQuotes({uri}: {uri: string}) { data?.pages .flatMap(page => page.posts.map(post => { - if (!AppBskyFeedPost.isRecord(post.record) || !moderationOpts) { + if ( + !bsky.dangerousIsType( + post.record, + AppBskyFeedPost.isRecord, + ) || + !moderationOpts + ) { return null } const moderation = moderatePost(post, moderationOpts) diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index 611eb7c43a..4c2db9eda0 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -28,7 +28,7 @@ function renderItem({ ) } -function keyExtractor(item: ActorDefs.ProfileViewBasic) { +function keyExtractor(item: ActorDefs.ProfileView) { return item.did } diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index 32ec6d6deb..07092caed6 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -43,7 +43,7 @@ function renderItem({ ) } -function keyExtractor(item: ActorDefs.ProfileViewBasic) { +function keyExtractor(item: ActorDefs.ProfileView) { return item.did } diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx index b2c1908673..9371038aa6 100644 --- a/src/view/com/profile/ProfileFollows.tsx +++ b/src/view/com/profile/ProfileFollows.tsx @@ -39,7 +39,7 @@ function renderItem({ ) } -function keyExtractor(item: ActorDefs.ProfileViewBasic) { +function keyExtractor(item: ActorDefs.ProfileView) { return item.did } diff --git a/src/view/com/util/List.web.tsx b/src/view/com/util/List.web.tsx index 441297f120..bc3d3de27a 100644 --- a/src/view/com/util/List.web.tsx +++ b/src/view/com/util/List.web.tsx @@ -28,8 +28,10 @@ import * as Layout from '#/components/Layout' export type ListMethods = { scrollToTop: () => void - scrollToOffset: (options: {animated: boolean; offset: number}) => void - scrollToEnd: (options?: {animated?: boolean}) => void + // Signature kept compatible with FlatList's scrollToOffset (the native + // ListMethods type) so callers stay platform-agnostic. + scrollToOffset: (options: {animated?: boolean | null; offset: number}) => void + scrollToEnd: (options?: {animated?: boolean | null}) => void // Signature kept compatible with FlatList's scrollToIndex (the native // ListMethods type) so callers stay platform-agnostic. viewOffset is // accepted for parity but not currently used by the web implementation. @@ -40,7 +42,8 @@ export type ListMethods = { viewPosition?: number }) => void } -export type ListProps = Omit< +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ListProps = Omit< FlatListProps, | 'onScroll' // Use ScrollContext instead. | 'refreshControl' // Pass refreshing and/or onRefresh instead. @@ -59,7 +62,7 @@ export type ListProps = Omit< */ sideBorders?: boolean } -export type ListRef = React.RefObject +export type ListRef = React.RefObject const ON_ITEM_SEEN_WAIT_DURATION = 0.5e3 // when we consider post to be "seen" const ON_ITEM_SEEN_INTERSECTION_OPTS = { @@ -248,7 +251,13 @@ function ListImpl( getScrollableNode()?.scrollTo({top: 0}) }, - scrollToOffset({animated, offset}: {animated: boolean; offset: number}) { + scrollToOffset({ + animated, + offset, + }: { + animated?: boolean | null + offset: number + }) { getScrollableNode()?.scrollTo({ left: 0, top: offset, diff --git a/src/view/com/util/listNativeTag.ts b/src/view/com/util/listNativeTag.ts new file mode 100644 index 0000000000..c4580a9ae0 --- /dev/null +++ b/src/view/com/util/listNativeTag.ts @@ -0,0 +1,12 @@ +import {findNodeHandle} from 'react-native' + +import {type ListMethods} from './List' + +/** + * Returns the native view tag backing a List, for handing to native code + * (e.g. the pager's scrollViewTag). Always null on web. + */ +export function findListNativeTag(list: ListMethods | null): number | null { + if (!list) return null + return findNodeHandle(list) +} diff --git a/src/view/com/util/listNativeTag.web.ts b/src/view/com/util/listNativeTag.web.ts new file mode 100644 index 0000000000..e8149cf4e7 --- /dev/null +++ b/src/view/com/util/listNativeTag.web.ts @@ -0,0 +1,9 @@ +import {type ListMethods} from './List' + +/** + * Returns the native view tag backing a List, for handing to native code + * (e.g. the pager's scrollViewTag). Always null on web. + */ +export function findListNativeTag(_list: ListMethods | null): number | null { + return null +} diff --git a/tsconfig.check.android.json b/tsconfig.check.android.json new file mode 100644 index 0000000000..3aa14b5615 --- /dev/null +++ b/tsconfig.check.android.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.check.json", + "compilerOptions": { + /* + * Mirrors Metro's resolution order for platform=android. + */ + "moduleSuffixes": [".android", ".native", ""] + }, + /* + * Narrows the base exclude so the *.android-check.d.ts ambient shims + * apply to this pass. + */ + "exclude": ["src/platform/*.web-check.d.ts"] +} diff --git a/tsconfig.check.ios.json b/tsconfig.check.ios.json new file mode 100644 index 0000000000..c91d6f804e --- /dev/null +++ b/tsconfig.check.ios.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.check.json", + "compilerOptions": { + /* + * Mirrors Metro's resolution order for platform=ios. + */ + "moduleSuffixes": [".ios", ".native", ""] + } +} diff --git a/tsconfig.check.json b/tsconfig.check.json index f9f1370f6d..6a3a0ed03d 100644 --- a/tsconfig.check.json +++ b/tsconfig.check.json @@ -1,4 +1,12 @@ { "extends": "./tsconfig.json", - "include": ["src", "modules", "app.config.js"] + "include": ["src", "modules", "app.config.js"], + /* + * Ambient check shims apply only to the platform pass that includes them + * back (tsconfig.check.android.json, tsconfig.check.web.json). + */ + "exclude": [ + "src/platform/*.android-check.d.ts", + "src/platform/*.web-check.d.ts" + ] } diff --git a/tsconfig.check.web.json b/tsconfig.check.web.json new file mode 100644 index 0000000000..156e51f648 --- /dev/null +++ b/tsconfig.check.web.json @@ -0,0 +1,24 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleSuffixes": [".web", ""], + "paths": { + "#/*": ["./src/*"], + "crypto": ["./src/platform/crypto.ts"], + /* + * expo-file-system/legacy resolves to the package's raw TypeScript + * source, whose internals break under .web module suffixes. Point it + * at the compiled declarations, where skipLibCheck applies. + */ + "expo-file-system/legacy": [ + "./node_modules/expo-file-system/build/legacy/index.d.ts" + ] + } + }, + "include": [ + "index.web.ts", + "src/**/*.web.ts", + "src/**/*.web.tsx", + "src/platform/*.web-check.d.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index c3f559d917..2f69ed70af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "jsx": "react-jsx", "module": "esnext", + "moduleSuffixes": [".ios", ".android", ".native", ""], "lib": ["dom", "esnext"], "types": ["node", "jest"], "paths": {