From 6f46927c28a5a18a08760a55b4c08415bc9579d6 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:39:46 -0700 Subject: [PATCH 01/22] Overfetch trending topics before filtering (#11477) --- src/components/interstitials/Trending.tsx | 154 ------------------ .../Search/modules/ExploreTrendingTopics.tsx | 6 +- .../queries/trending/useGetTrendsQuery.ts | 15 +- src/view/com/posts/PostFeed.tsx | 7 - .../shell/desktop/SidebarTrendingTopics.tsx | 1 - 5 files changed, 14 insertions(+), 169 deletions(-) delete mode 100644 src/components/interstitials/Trending.tsx diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx deleted file mode 100644 index 0b94064dac..0000000000 --- a/src/components/interstitials/Trending.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import {useCallback} from 'react' -import {ScrollView, View} from 'react-native' -import {useLingui} from '@lingui/react/macro' - -import { - useTrendingSettings, - useTrendingSettingsApi, -} from '#/state/preferences/trending' -import {useGetTrendsQuery} from '#/state/queries/trending/useGetTrendsQuery' -import {useTrendingConfig} from '#/state/service-config' -import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' -import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture' -import {atoms as a, useGutters, useTheme} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' -import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' -import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending' -import * as Prompt from '#/components/Prompt' -import {TrendingTopicLink} from '#/components/TrendingTopics' -import {Text} from '#/components/Typography' -import {useAnalytics} from '#/analytics' - -const TRENDING_LIMIT = 14 - -export function TrendingInterstitial() { - const {enabled} = useTrendingConfig() - const {trendingDisabled} = useTrendingSettings() - return enabled && !trendingDisabled ? : null -} - -export function Inner() { - const t = useTheme() - const {t: l} = useLingui() - const ax = useAnalytics() - const gutters = useGutters([0, 'base', 0, 'base']) - const trendingPrompt = Prompt.usePromptControl() - const {setTrendingDisabled} = useTrendingSettingsApi() - const { - data: trending, - error, - isLoading, - } = useGetTrendsQuery({ - limit: TRENDING_LIMIT, - refetchOnWindowFocus: true, - }) - const noTopics = !isLoading && !error && !trending?.trends?.length - - const onConfirmHide = useCallback(() => { - ax.metric('trendingTopics:hide', {context: 'interstitial'}) - setTrendingDisabled(true) - }, [ax, setTrendingDisabled]) - - return error || noTopics ? null : ( - - - - - - - - {isLoading ? ( - - - - - - - - {' '} - - - ) : !trending?.trends ? null : ( - <> - {trending.trends.map((topic, index) => { - const rank = index + 1 - return ( - { - ax.metric('trendingTopic:click', { - context: 'interstitial', - rank, - recId: trending.recId, - }) - }}> - - - {topic.topic} - - - - ) - })} - - - )} - - - - - - - ) -} diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index e8bfeed54a..72ca638dab 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -11,6 +11,7 @@ import { useTrendingSettingsApi, } from '#/state/preferences/trending' import { + DEFAULT_FETCH_LIMIT, DEFAULT_LIMIT, useGetTrendsQuery, } from '#/state/queries/trending/useGetTrendsQuery' @@ -57,7 +58,10 @@ function Inner() { error, isLoading, isRefetching, - } = useGetTrendsQuery({limit: topicCount}) + } = useGetTrendsQuery({ + fetchLimit: Math.min(topicCount * 2, DEFAULT_FETCH_LIMIT), + limit: topicCount, + }) const noTopics = !isLoading && !error && !trending?.trends?.length const showLoading = isLoading || isRefetching diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index fabeccd478..9c8e851f43 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -14,8 +14,10 @@ import {useAppviewClient} from '#/state/session' import {app} from '#/lexicons' export const DEFAULT_LIMIT = 5 +export const DEFAULT_FETCH_LIMIT = 20 type QueryProps = { + fetchLimit?: number limit?: number refetchOnWindowFocus?: boolean } @@ -29,12 +31,13 @@ function dedupe(trends: T[]): T[] { }) } -export const createGetTrendsQueryKey = (limit?: number) => - limit === undefined ? ['trends'] : ['trends', {limit}] +export const createGetTrendsQueryKey = (fetchLimit?: number) => + fetchLimit === undefined ? ['trends'] : ['trends', {limit: fetchLimit}] export function useGetTrendsQuery(props: QueryProps = {}) { const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() + const fetchLimit = props.fetchLimit ?? DEFAULT_FETCH_LIMIT const limit = props.limit ?? DEFAULT_LIMIT const mutedWords = useMemo(() => { return preferences?.moderationPrefs?.mutedWords || [] @@ -44,13 +47,13 @@ export function useGetTrendsQuery(props: QueryProps = {}) { enabled: !!preferences, refetchOnWindowFocus: props.refetchOnWindowFocus, staleTime: STALE.MINUTES.THREE, - queryKey: createGetTrendsQueryKey(limit), + queryKey: createGetTrendsQueryKey(fetchLimit), queryFn: async () => { const contentLangs = getContentLanguages().join(',') const data = await client.call( app.bsky.unspecced.getTrends, { - limit, + limit: fetchLimit, }, { headers: { @@ -75,10 +78,10 @@ export function useGetTrendsQuery(props: QueryProps = {}) { text: `${t.topic} ${t.displayName} ${t.category}`, }) }), - ), + ).slice(0, limit), } }, - [mutedWords], + [limit, mutedWords], ), }) } diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 62bf3209c9..38fce08344 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -62,7 +62,6 @@ import { PostFeedVideoGridRowPlaceholder, } from '#/components/feeds/PostFeedVideoGridRow' import {FeedTrendingTopicsInterstitial} from '#/components/interstitials/FeedTrendingTopics' -import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' import {RichText} from '#/components/RichText' @@ -146,10 +145,6 @@ type FeedRow = type: 'interstitialProgressGuide' key: string } - | { - type: 'interstitialTrending' - key: string - } | { type: 'interstitialFeedTrendingTopics' key: string @@ -853,8 +848,6 @@ let PostFeed = ({ return } else if (row.type === 'ageAssuranceBanner') { return - } else if (row.type === 'interstitialTrending') { - return } else if (row.type === 'interstitialFeedTrendingTopics') { return ( diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index 25f2510798..cd07fa83ac 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -46,7 +46,6 @@ function Inner() { error, isLoading, } = useGetTrendsQuery({ - limit: DEFAULT_LIMIT, refetchOnWindowFocus: true, }) const noTopics = !isLoading && !error && !trending?.trends?.length From 6b35d3de1c5b80585dfa040c81809aea4ab2498e Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:40:15 -0700 Subject: [PATCH 02/22] Revert "Send beta user state with AppView requests (#11474)" (#11510) --- src/analytics/index.tsx | 16 ++--- src/screens/Settings/BetaFeaturesSettings.tsx | 4 +- src/state/preferences/beta-user-cache.ts | 64 ------------------- src/state/preferences/beta-user-sync.tsx | 14 ++-- src/state/session/__tests__/clients-test.ts | 47 -------------- src/state/session/clients.ts | 29 +-------- src/storage/schema.ts | 7 +- 7 files changed, 22 insertions(+), 159 deletions(-) delete mode 100644 src/state/preferences/beta-user-cache.ts diff --git a/src/analytics/index.tsx b/src/analytics/index.tsx index d765c46e2a..07af41473e 100644 --- a/src/analytics/index.tsx +++ b/src/analytics/index.tsx @@ -9,10 +9,6 @@ import {Platform} from 'react-native' import {type Result, type WidenPrimitives} from '@growthbook/growthbook-react' import {Logger} from '#/logger' -import { - getCachedIsBetaUser, - subscribeToCachedIsBetaUser, -} from '#/state/preferences/beta-user-cache' import { Features, features as feats, @@ -37,7 +33,7 @@ import {type Metrics, metrics} from '#/analytics/metrics' import * as refParams from '#/analytics/misc/refParams' import * as env from '#/env' import {useGeolocationServiceResponse} from '#/geolocation/service' -import {device} from '#/storage' +import {account, device} from '#/storage' export * as utils from '#/analytics/utils' export const features = {init, refresh} @@ -135,7 +131,7 @@ export const setupDeviceId = getAndMigrateDeviceId() /** * Reads the per-account cached `isBetaUser` flag for `did`, kept in sync with - * PDS preference query results and the beta settings toggle. + * writes from `BetaUserStorageSync` and the beta settings toggle. * * This deliberately does not use `useStorage`, whose `useState` seeds once and * only updates via the change listener. The consuming `AnalyticsContext` lives @@ -150,13 +146,17 @@ function useAccountIsBetaUser(did: string | undefined): boolean | undefined { const subscribe = useCallback( (onChange: () => void) => { if (!did) return () => {} - return subscribeToCachedIsBetaUser(did, onChange) + const sub = account.addOnValueChangedListener( + [did, 'isBetaUser'], + onChange, + ) + return () => sub.remove() }, [did], ) const getSnapshot = useCallback(() => { if (!did) return undefined - return getCachedIsBetaUser(did) + return account.get([did, 'isBetaUser']) }, [did]) return useSyncExternalStore(subscribe, getSnapshot) } diff --git a/src/screens/Settings/BetaFeaturesSettings.tsx b/src/screens/Settings/BetaFeaturesSettings.tsx index a8d2aeba58..28f561e719 100644 --- a/src/screens/Settings/BetaFeaturesSettings.tsx +++ b/src/screens/Settings/BetaFeaturesSettings.tsx @@ -5,7 +5,6 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' import {logger} from '#/logger' -import {setCachedIsBetaUser} from '#/state/preferences/beta-user-cache' import { usePreferencesQuery, useSetIsBetaUserMutation, @@ -26,6 +25,7 @@ import {Text} from '#/components/Typography' import {features, useAnalytics} from '#/analytics' import {getTargetedFeatures} from '#/analytics/features' import {IS_WEB} from '#/env' +import {account} from '#/storage' type Props = NativeStackScreenProps< CommonNavigatorParams, @@ -74,7 +74,7 @@ export function BetaFeaturesSettingsScreen({}: Props) { * account-specific. */ if (currentAccount) { - setCachedIsBetaUser(currentAccount.did, next) + account.set([currentAccount.did, 'isBetaUser'], next) } ax.metric('betaFeatures:toggle', { enabled: next, diff --git a/src/state/preferences/beta-user-cache.ts b/src/state/preferences/beta-user-cache.ts deleted file mode 100644 index c2a31e5334..0000000000 --- a/src/state/preferences/beta-user-cache.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {account} from '#/storage' - -const values = new Map() -const listeners = new Map void>>() - -/** - * Returns the last known beta-user preference for an account. - * - * The first read for a DID in this process is hydrated synchronously from - * persistent storage so cold starts retain the last value fetched from the - * PDS. Subsequent reads, including request-header reads, stay in memory. - */ -export function getCachedIsBetaUser(did: string): boolean | undefined { - if (!values.has(did)) { - try { - values.set(did, account.get([did, 'isBetaUser'])) - } catch { - values.set(did, undefined) - } - } - return values.get(did) -} - -/** - * Updates the runtime cache and its persistent cold-start snapshot. - * - * Call this only with a value confirmed by the PDS, which remains the source - * of truth for the preference. - */ -export function setCachedIsBetaUser(did: string, value: boolean): void { - if (getCachedIsBetaUser(did) === value) return - account.set([did, 'isBetaUser'], value) - values.set(did, value) - listeners.get(did)?.forEach(listener => listener()) -} - -/** - * Subscribe to runtime cache changes for one account. - */ -export function subscribeToCachedIsBetaUser( - did: string, - listener: () => void, -): () => void { - let didListeners = listeners.get(did) - if (!didListeners) { - didListeners = new Set() - listeners.set(did, didListeners) - } - didListeners.add(listener) - - return () => { - didListeners.delete(listener) - if (didListeners.size === 0) listeners.delete(did) - } -} - -/** - * Drops the in-memory value so the next read rehydrates from persistence. - * This is useful when persistence is changed outside this cache. - */ -export function invalidateCachedIsBetaUser(did: string): void { - if (!values.delete(did)) return - listeners.get(did)?.forEach(listener => listener()) -} diff --git a/src/state/preferences/beta-user-sync.tsx b/src/state/preferences/beta-user-sync.tsx index edad7fc911..f8cead4955 100644 --- a/src/state/preferences/beta-user-sync.tsx +++ b/src/state/preferences/beta-user-sync.tsx @@ -2,12 +2,12 @@ import {useEffect} from 'react' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSession} from '#/state/session' -import {getCachedIsBetaUser, setCachedIsBetaUser} from './beta-user-cache' +import {account} from '#/storage' /** - * Caches `bskyAppState.isBetaUser` from preferences in memory and synchronous - * device storage so analytics can read it at init (before beta-gated features - * are evaluated). Scoped per account, since `isBetaUser` is account-specific: + * Caches `bskyAppState.isBetaUser` from preferences into synchronous device + * storage so analytics can read it at init (before beta-gated features are + * evaluated). Scoped per account, since `isBetaUser` is account-specific: * a global cache would let one account's value leak into another after a * switch, until the new account's preferences loaded. Must be mounted below * `QueryProvider`, since the analytics providers that consume the cached value @@ -27,10 +27,10 @@ export function BetaUserStorageSync() { if (isBetaUser === undefined) return /* * Guard against a redundant write on every warm boot. Writing triggers the - * cache change listener, which re-renders the analytics subtree. + * storage change listener, which re-renders the analytics subtree. */ - if (getCachedIsBetaUser(did) === isBetaUser) return - setCachedIsBetaUser(did, isBetaUser) + if (account.get([did, 'isBetaUser']) === isBetaUser) return + account.set([did, 'isBetaUser'], isBetaUser) }, [did, isBetaUser]) return null diff --git a/src/state/session/__tests__/clients-test.ts b/src/state/session/__tests__/clients-test.ts index 1dfa93e0ea..50cf70fede 100644 --- a/src/state/session/__tests__/clients-test.ts +++ b/src/state/session/__tests__/clients-test.ts @@ -14,12 +14,7 @@ jest.mock('jwt-decode', () => ({ })) import {BLUESKY_PROXY_HEADER, CHAT_PROXY_SERVICE} from '#/lib/constants' -import { - invalidateCachedIsBetaUser, - setCachedIsBetaUser, -} from '#/state/preferences/beta-user-cache' import {app, chat, com} from '#/lexicons' -import {account} from '#/storage' import {configureGlobalAppLabelers} from '../additional-moderation-authorities' import { buildAppviewClient, @@ -89,8 +84,6 @@ describe('buildAppviewClient', () => { beforeEach(() => { fetchMock = makeProfileFetch() configureGlobalAppLabelers([]) - account.remove([DID, 'isBetaUser']) - invalidateCachedIsBetaUser(DID) }) it('passes through the session did', () => { @@ -118,46 +111,6 @@ describe('buildAppviewClient', () => { ).toBe(BLUESKY_PROXY_HEADER.get()) }) - it.each([true, false])( - 'emits the current beta user header when the cached value is %s', - async isBetaUser => { - const client = buildAppviewClient(makeSession(fetchMock)) - setCachedIsBetaUser(DID, isBetaUser) - - await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) - - expect( - headersFor(fetchMock, 'app.bsky.actor.getProfile').get( - 'x-bsky-is-beta-user', - ), - ).toBe(String(isBetaUser)) - }, - ) - - it('omits the beta user header when the preference is not cached', async () => { - const client = buildAppviewClient(makeSession(fetchMock)) - - await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) - - expect( - headersFor(fetchMock, 'app.bsky.actor.getProfile').get( - 'x-bsky-is-beta-user', - ), - ).toBeNull() - }) - - it('reads the persisted beta preference only once', async () => { - account.set([DID, 'isBetaUser'], true) - const getSpy = jest.spyOn(account, 'get') - const client = buildAppviewClient(makeSession(fetchMock)) - - await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) - await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) - - expect(getSpy).toHaveBeenCalledTimes(1) - getSpy.mockRestore() - }) - it('emits an account subscription exactly once', async () => { const client = buildAppviewClient(makeSession(fetchMock)) client.setLabelers(['did:plc:labeler']) diff --git a/src/state/session/clients.ts b/src/state/session/clients.ts index c685bf90b2..026b2d8bd8 100644 --- a/src/state/session/clients.ts +++ b/src/state/session/clients.ts @@ -7,33 +7,8 @@ import { PUBLIC_BSKY_SERVICE, } from '#/lib/constants' import {createLexClient} from '#/lib/lexClient' -import {getCachedIsBetaUser} from '#/state/preferences/beta-user-cache' import {networkAwareFetch} from './network' -const IS_BETA_USER_HEADER = 'X-Bsky-Is-Beta-User' - -/** - * Add account-scoped headers to appview requests. - * - * Values are read from memory per request so preference changes are reflected - * immediately without rebuilding the session bundle. - */ -function withAppviewRequestHeaders(agent: Agent): Agent { - return { - get did() { - return agent.did - }, - fetchHandler(path, init) { - const headers = new Headers(init?.headers) - const isBetaUser = agent.did ? getCachedIsBetaUser(agent.did) : undefined - if (isBetaUser !== undefined) { - headers.set(IS_BETA_USER_HEADER, String(isBetaUser)) - } - return agent.fetchHandler(path, {...init, headers}) - }, - } -} - /** * Build the signed-in appview {@link Client}. * @@ -53,9 +28,7 @@ function withAppviewRequestHeaders(agent: Agent): Agent { * fetch, which is `networkAwareFetch` wrapped in the disposal kill switch. */ export function buildAppviewClient(agent: Agent): Client { - return createLexClient(withAppviewRequestHeaders(agent), { - service: BLUESKY_PROXY_HEADER.get(), - }) + return createLexClient(agent, {service: BLUESKY_PROXY_HEADER.get()}) } /** diff --git a/src/storage/schema.ts b/src/storage/schema.ts index c8620a423f..53b4ffb022 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -98,9 +98,10 @@ export type Account = { recentGifs?: Gif[] /** - * Persistent cold-start snapshot of `bskyAppState.isBetaUser`. Hydrates the - * runtime cache so the GrowthBook attribute and request header are available - * synchronously before preferences load. Written back when preferences load. + * Cached from preferences (`bskyAppState.isBetaUser`) so the GrowthBook + * `isBetaUser` attribute can be set synchronously at analytics init, before + * beta-gated features (e.g. SearchV2Enable) are first evaluated. Written back + * when preferences load. * * Scoped per account, since `isBetaUser` is account-specific preference data. * Reading it globally would let a beta account's value leak into a non-beta From 95f5360012ee034443e2142606602f9657d6ed57 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 25 Aug 2026 03:28:56 +0300 Subject: [PATCH 03/22] Fix iOS message input height after sending (#11533) --- src/components/forms/AutosizedTextarea.tsx | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/forms/AutosizedTextarea.tsx b/src/components/forms/AutosizedTextarea.tsx index d81265f4da..414bdecda7 100644 --- a/src/components/forms/AutosizedTextarea.tsx +++ b/src/components/forms/AutosizedTextarea.tsx @@ -8,7 +8,7 @@ import { import {mergeRefs} from '#/lib/merge-refs' import {atoms as a, extractPadding, useAlf, web} from '#/alf' import {normalizeTextStyles} from '#/alf/typography' -import {IS_ANDROID, IS_IOS, IS_WEB} from '#/env' +import {IS_ANDROID, IS_IOS, IS_NATIVE, IS_WEB} from '#/env' export type AutosizedTextareaProps = Omit & { ref?: React.Ref @@ -18,13 +18,14 @@ export type AutosizedTextareaProps = Omit & { onUpdateHeight?: (height: number) => void /** * In some cases, like on native, we may not pass in an actual `value` prop. - * This prop allows Android to know if the field was cleared and thereby - * reset its height. This is a small hack required because we need to - * explicitly set the `{height: nativeHeight}` on Android. + * This prop allows native platforms to know if the field was cleared and + * thereby reset its height. This is a small hack required because Android's + * height is driven by state, while iOS needs an explicit minimum height when + * cleared programmatically. * - * If you notice height calcuation issues after clearing the field on - * Android, check if this value is being populated and cleared properly in - * the parent component. + * If you notice height calculation issues after clearing the field on a + * native platform, check if this value is being populated and cleared + * properly in the parent component. */ rawValue?: string } @@ -137,13 +138,12 @@ export function AutosizedTextarea({ } /* - * Manual height clearing required for Android because we're forced to set - * `{height: nativeHeight}` on Android, and even though `onContentSizeChange` - * fires, the height matches the existing height and we aren't able to reset. + * Reset native height state after a programmatic clear. Android uses it as + * the explicit input height, while iOS uses it to decide when to scroll. */ const prevRawValue = useRef(rawValue || '') useEffect(() => { - if (!IS_ANDROID) return // everything else is fine + if (!IS_NATIVE) return if (rawValue === undefined) return // uncontrolled if (prevRawValue.current?.length && rawValue === '') { setNativeHeight(minInputHeight) @@ -176,7 +176,8 @@ export function AutosizedTextarea({ wordBreak: 'break-word', }), style, - IS_ANDROID ? {height: nativeHeight} : {}, + IS_ANDROID && {height: nativeHeight}, + IS_IOS && rawValue === '' && {height: minInputHeight}, ]} {...rest} ref={mergeRefs([ From cf2684b0d1b6217291bbbba25039ef8de9c4e56c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 25 Aug 2026 03:34:16 +0300 Subject: [PATCH 04/22] Send an integer exp for video upload service auth tokens (#11529) Co-authored-by: Claude --- .../video/__tests__/upload.shared.test.ts | 70 +++++++++++++++++++ src/lib/media/video/multipart/upload.ts | 4 +- src/lib/media/video/upload.shared.ts | 38 +++++++++- src/lib/media/video/upload.ts | 8 ++- src/lib/media/video/upload.web.ts | 8 ++- 5 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 src/lib/media/video/__tests__/upload.shared.test.ts diff --git a/src/lib/media/video/__tests__/upload.shared.test.ts b/src/lib/media/video/__tests__/upload.shared.test.ts new file mode 100644 index 0000000000..7ab8b88ed3 --- /dev/null +++ b/src/lib/media/video/__tests__/upload.shared.test.ts @@ -0,0 +1,70 @@ +import {type Client} from '@atproto/lex' + +import {com} from '#/lexicons' +import { + getServiceAuthToken, + SERVICE_AUTH_TTL_SEC, + serviceAuthExp, +} from '../upload.shared' + +function createClient() { + const call = jest.fn().mockResolvedValue({token: 'token'}) + return {client: {call} as unknown as Client, call} +} + +describe('serviceAuthExp', () => { + it('returns an integer even when the clock has sub-second precision', () => { + jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_500) + expect(serviceAuthExp()).toBe(1_700_000_000 + SERVICE_AUTH_TTL_SEC) + expect(Number.isInteger(serviceAuthExp())).toBe(true) + }) + + it('accepts a custom ttl and keeps the result integral', () => { + jest.spyOn(Date, 'now').mockReturnValue(1_700_000_000_999) + expect(serviceAuthExp(90.7)).toBe(1_700_000_090) + }) +}) + +describe('getServiceAuthToken', () => { + it('floors a fractional exp before sending it', async () => { + const {client, call} = createClient() + await getServiceAuthToken({ + client, + aud: 'did:web:video.bsky.app', + lxm: 'com.atproto.repo.uploadBlob', + exp: 1_700_001_800.5, + }) + expect(call).toHaveBeenCalledWith(com.atproto.server.getServiceAuth, { + aud: 'did:web:video.bsky.app', + lxm: 'com.atproto.repo.uploadBlob', + exp: 1_700_001_800, + }) + }) + + it('leaves exp undefined when the caller omits it', async () => { + const {client, call} = createClient() + await getServiceAuthToken({ + client, + aud: 'did:web:video.bsky.app', + lxm: 'app.bsky.video.getUploadLimits', + }) + expect(call).toHaveBeenCalledWith(com.atproto.server.getServiceAuth, { + aud: 'did:web:video.bsky.app', + lxm: 'app.bsky.video.getUploadLimits', + exp: undefined, + }) + }) + + it('rejects a non-finite exp rather than sending NaN', async () => { + const {client, call} = createClient() + await expect( + getServiceAuthToken({ + client, + aud: 'did:web:video.bsky.app', + lxm: 'com.atproto.repo.uploadBlob', + exp: NaN, + }), + ).rejects.toThrow('Invalid service auth exp') + expect(call).not.toHaveBeenCalled() + }) +}) diff --git a/src/lib/media/video/multipart/upload.ts b/src/lib/media/video/multipart/upload.ts index d3e5210873..3b0048ecd2 100644 --- a/src/lib/media/video/multipart/upload.ts +++ b/src/lib/media/video/multipart/upload.ts @@ -5,7 +5,7 @@ import {AbortError} from '#/lib/async/cancelable' import {type CompressedVideo} from '#/lib/media/video/types' import {shouldRetryError} from '#/lib/strings/errors' import {type app} from '#/lexicons' -import {getServiceAuthToken} from '../upload.shared' +import {getServiceAuthToken, serviceAuthExp} from '../upload.shared' import {mimeToExt} from '../util' import { abortUpload, @@ -280,7 +280,7 @@ function createTokenProvider( async function get(forceRefresh = false) { if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token if (!refresh) { - const exp = Math.floor(Date.now() / 1000) + 60 * 30 + const exp = serviceAuthExp() refresh = getServiceAuthTokenWithRetry(client, dispatchUrl, exp, signal) .then(nextToken => { token = nextToken diff --git a/src/lib/media/video/upload.shared.ts b/src/lib/media/video/upload.shared.ts index 9e8d99a185..1ea0c62a9c 100644 --- a/src/lib/media/video/upload.shared.ts +++ b/src/lib/media/video/upload.shared.ts @@ -27,6 +27,11 @@ export async function getServiceAuthToken({ dispatchUrl?: string | URL aud?: string lxm: NsidString + /** + * Unix timestamp in *seconds* at which the token expires. Fractional values + * are floored - see {@link toIntegerExp}. Defaults to the server's own + * short expiry when omitted. + */ exp?: number }) { let resolvedAud = aud @@ -43,11 +48,42 @@ export async function getServiceAuthToken({ const {token} = await client.call(com.atproto.server.getServiceAuth, { aud: resolvedAud as DidString, lxm, - exp, + exp: exp === undefined ? undefined : toIntegerExp(exp), }) return token } +/** + * Default lifetime for the video upload service auth token. Long enough to + * cover a slow upload of a large file, short enough to limit the damage if the + * token leaks. + */ +export const SERVICE_AUTH_TTL_SEC = 60 * 30 + +/** + * Build a service auth `exp` claim `ttlSec` seconds from now. + * + * Always use this instead of hand-rolling the arithmetic: `Date.now()` is in + * milliseconds, and dividing by 1000 without flooring yields a fractional + * timestamp that the endpoint rejects. + */ +export function serviceAuthExp(ttlSec: number = SERVICE_AUTH_TTL_SEC) { + return Math.floor(Date.now() / 1000) + Math.floor(ttlSec) +} + +/** + * The lexicon types `exp` as an integer and it is serialized straight into the + * query string, so a fractional value fails validation and the upload dies + * before it starts. Floor here, at the single chokepoint every caller goes + * through, so a call site that forgets to cannot reintroduce the bug. + */ +function toIntegerExp(exp: number) { + if (!Number.isFinite(exp)) { + throw new Error(`Invalid service auth exp: ${exp}`) + } + return Math.floor(exp) +} + export async function getVideoUploadLimits(client: Client, i18n: I18n) { const token = await getServiceAuthToken({ client, diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts index 287c8f09c6..d5a2af5146 100644 --- a/src/lib/media/video/upload.ts +++ b/src/lib/media/video/upload.ts @@ -13,7 +13,11 @@ import { import {Features, features} from '#/analytics/features' import {type app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' -import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' +import { + getServiceAuthToken, + getVideoUploadLimits, + serviceAuthExp, +} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ @@ -72,7 +76,7 @@ export async function uploadVideo({ client, dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', - exp: Date.now() / 1000 + 60 * 30, // 30 minutes + exp: serviceAuthExp(), }) const uploadTask = createUploadTask( uri, diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts index 90c10a0684..b0fcbaaf09 100644 --- a/src/lib/media/video/upload.web.ts +++ b/src/lib/media/video/upload.web.ts @@ -12,7 +12,11 @@ import { import {Features, features} from '#/analytics/features' import {type app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' -import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' +import { + getServiceAuthToken, + getVideoUploadLimits, + serviceAuthExp, +} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ @@ -79,7 +83,7 @@ export async function uploadVideo({ client, dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', - exp: Date.now() / 1000 + 60 * 30, // 30 minutes + exp: serviceAuthExp(), }) if (signal.aborted) { From f1190ce6815a32c15e4ce49e17c0aac450746d73 Mon Sep 17 00:00:00 2001 From: surfdude29 <149612116+surfdude29@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:34:30 +0100 Subject: [PATCH 05/22] Update tech stack versions and fix markdown in CLAUDE.md (#11530) Co-authored-by: Claude Opus 5 --- CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fbe87d5287..d90cbf7cb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,8 @@ Bluesky Social is a cross-platform social media application built with React Nat **Tech Stack:** -- React 19.1 -- React Native 0.81 with Expo 54 +- React 19.2 +- React Native 0.86 with Expo 57 - TypeScript 7 - React Navigation 7 for routing - TanStack Query (React Query) for data fetching @@ -561,7 +561,7 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as: 1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful -2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `` +2. **Translations**: Wrap ALL user-facing strings with the `` l`…` `` macro or the `` component 3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles From 56bff455463f7833a945c9afd180f67255296679 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:40:18 -0700 Subject: [PATCH 06/22] Bump the actions group with 2 updates (#11537) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-and-push-bskyweb-aws.yaml | 2 +- .github/workflows/build-and-push-bskyweb-ghcr.yaml | 4 ++-- .github/workflows/build-and-push-embedr-aws.yaml | 2 +- .github/workflows/build-and-push-link-aws.yaml | 2 +- .github/workflows/build-and-push-ogcard-aws.yaml | 2 +- .github/workflows/claude-mention.yml | 2 +- .github/workflows/claude-review.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml index eb2c9b7bbb..8fffe1d17b 100644 --- a/.github/workflows/build-and-push-bskyweb-aws.yaml +++ b/.github/workflows/build-and-push-bskyweb-aws.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: 🔑 Log into registry ${{ env.REGISTRY }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/build-and-push-bskyweb-ghcr.yaml b/.github/workflows/build-and-push-bskyweb-ghcr.yaml index 66652d5c9d..d996cebc6a 100644 --- a/.github/workflows/build-and-push-bskyweb-ghcr.yaml +++ b/.github/workflows/build-and-push-bskyweb-ghcr.yaml @@ -41,7 +41,7 @@ jobs: - name: ⬇️ Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Build ${{ matrix.image }} uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: @@ -65,7 +65,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: 🔑 Log into registry ${{ env.REGISTRY }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/build-and-push-embedr-aws.yaml b/.github/workflows/build-and-push-embedr-aws.yaml index 9947935261..59daa8b4a0 100644 --- a/.github/workflows/build-and-push-embedr-aws.yaml +++ b/.github/workflows/build-and-push-embedr-aws.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: 🔑 Log into registry ${{ env.REGISTRY }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/build-and-push-link-aws.yaml b/.github/workflows/build-and-push-link-aws.yaml index 60a043faa6..11c335c307 100644 --- a/.github/workflows/build-and-push-link-aws.yaml +++ b/.github/workflows/build-and-push-link-aws.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: 🔑 Log into registry ${{ env.REGISTRY }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/build-and-push-ogcard-aws.yaml b/.github/workflows/build-and-push-ogcard-aws.yaml index ec5eee6e83..79f70e2b0c 100644 --- a/.github/workflows/build-and-push-ogcard-aws.yaml +++ b/.github/workflows/build-and-push-ogcard-aws.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 🔧 Setup Docker buildx - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: 🔑 Log into registry ${{ env.REGISTRY }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 diff --git a/.github/workflows/claude-mention.yml b/.github/workflows/claude-mention.yml index e76e8cea45..9c06aa5886 100644 --- a/.github/workflows/claude-mention.yml +++ b/.github/workflows/claude-mention.yml @@ -60,7 +60,7 @@ jobs: fetch-depth: 1 - name: 🤖 Claude - uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190 + uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194 env: ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev with: diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index ba82c487e4..309ae4807d 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -45,7 +45,7 @@ jobs: fetch-depth: 1 - name: 🤖 Claude review - uses: anthropics/claude-code-action@5ef2e550a465a721f4f45e4a7d3c340c873e1dcc # v1.0.190 + uses: anthropics/claude-code-action@459ad358ae43fea66bfefd0a1f8d840b4b9791fb # v1.0.194 env: ANTHROPIC_BASE_URL: https://agentgateway.k1.prod.bsky.dev with: From 5f3d24efab170d72ab33ce08b84ead3d09b779a8 Mon Sep 17 00:00:00 2001 From: pfrazee <1270099+pfrazee@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:30:39 +0000 Subject: [PATCH 07/22] Nightly source-language update --- src/locale/locales/en/messages.po | 125 ++++++++++++++---------------- 1 file changed, 59 insertions(+), 66 deletions(-) diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index bb48e2dacf..ad67910416 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -198,7 +198,7 @@ msgstr "" #. Number of users (always at least 25) who have joined Bluesky using a specific starter pack #. placeholder {0}: starterPack.joinedAllTimeCount || 0 -#: src/screens/StarterPack/StarterPackScreen.tsx:497 +#: src/screens/StarterPack/StarterPackScreen.tsx:500 msgid "{0, plural, other {# people have}} joined Bluesky via this starter pack!" msgstr "" @@ -654,7 +654,7 @@ msgstr "" #. '{postCount} {posts}', e.g., '1.2K posts' #. '{postCount} {posts}', e.g., '1.2K posts' #: src/components/interstitials/FeedTrendingTopics.tsx:246 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:190 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:194 msgid "{formattedPostCount} {postCount, plural, one {post} other {posts}}" msgstr "{formattedPostCount} {postCount, plural, one {post} other {posts}}" @@ -762,7 +762,7 @@ msgstr "" #. The trending topic rank, i.e. "1. March Madness", "2. The Bachelor" #. The trending topic rank, i.e. "1. March Madness", "2. The Bachelor" #: src/components/interstitials/FeedTrendingTopics.tsx:231 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:165 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:169 msgid "{rank}." msgstr "" @@ -1440,7 +1440,7 @@ msgid "All {0}" msgstr "All {0}" #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:98 -#: src/screens/StarterPack/StarterPackScreen.tsx:398 +#: src/screens/StarterPack/StarterPackScreen.tsx:401 msgid "All accounts have been followed!" msgstr "" @@ -1623,8 +1623,8 @@ msgstr "" #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:55 #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:83 -#: src/screens/StarterPack/StarterPackScreen.tsx:357 -#: src/screens/StarterPack/StarterPackScreen.tsx:384 +#: src/screens/StarterPack/StarterPackScreen.tsx:360 +#: src/screens/StarterPack/StarterPackScreen.tsx:387 msgid "An error occurred while trying to follow all" msgstr "" @@ -1909,7 +1909,7 @@ msgstr "" msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." msgstr "Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants." -#: src/screens/StarterPack/StarterPackScreen.tsx:677 +#: src/screens/StarterPack/StarterPackScreen.tsx:680 msgid "Are you sure you want to delete this starter pack?" msgstr "" @@ -2433,7 +2433,7 @@ msgstr "" #. placeholder {0}: trend.displayName #: src/components/interstitials/FeedTrendingTopics.tsx:203 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:147 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:151 msgid "Browse topic {0}" msgstr "" @@ -3448,7 +3448,7 @@ msgstr "Copy invite link" #: src/components/dms/ChatInvite/Root.tsx:92 #: src/components/StarterPack/ShareDialog.tsx:115 -#: src/screens/StarterPack/StarterPackScreen.tsx:637 +#: src/screens/StarterPack/StarterPackScreen.tsx:640 msgid "Copy link" msgstr "" @@ -3473,7 +3473,7 @@ msgstr "" msgid "Copy link to profile" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:630 +#: src/screens/StarterPack/StarterPackScreen.tsx:633 msgid "Copy link to starter pack" msgstr "" @@ -3628,7 +3628,7 @@ msgstr "" msgid "Create a list" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:613 +#: src/screens/StarterPack/StarterPackScreen.tsx:616 msgid "Create a list from this starter pack" msgstr "" @@ -3695,7 +3695,7 @@ msgstr "Create group chat" msgid "Create list" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:619 +#: src/screens/StarterPack/StarterPackScreen.tsx:622 msgid "Create list from members" msgstr "" @@ -3838,9 +3838,9 @@ msgstr "" #: src/components/PostControls/PostMenu/PostMenuItems.tsx:824 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:276 #: src/screens/Settings/AppPasswords.tsx:213 -#: src/screens/StarterPack/StarterPackScreen.tsx:608 -#: src/screens/StarterPack/StarterPackScreen.tsx:708 -#: src/screens/StarterPack/StarterPackScreen.tsx:787 +#: src/screens/StarterPack/StarterPackScreen.tsx:611 +#: src/screens/StarterPack/StarterPackScreen.tsx:711 +#: src/screens/StarterPack/StarterPackScreen.tsx:790 msgid "Delete" msgstr "" @@ -3911,12 +3911,12 @@ msgstr "" msgid "Delete post" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:602 -#: src/screens/StarterPack/StarterPackScreen.tsx:778 +#: src/screens/StarterPack/StarterPackScreen.tsx:605 +#: src/screens/StarterPack/StarterPackScreen.tsx:781 msgid "Delete starter pack" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:674 +#: src/screens/StarterPack/StarterPackScreen.tsx:677 msgid "Delete starter pack?" msgstr "" @@ -4337,7 +4337,7 @@ msgstr "" #: src/screens/Messages/components/EditTextButton.tsx:52 #: src/screens/Settings/AccountSettings.tsx:148 #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:250 -#: src/screens/StarterPack/StarterPackScreen.tsx:597 +#: src/screens/StarterPack/StarterPackScreen.tsx:600 #: src/screens/StarterPack/Wizard/index.tsx:327 #: src/screens/StarterPack/Wizard/index.tsx:332 msgid "Edit" @@ -4441,7 +4441,7 @@ msgstr "" msgid "Edit Profile" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:589 +#: src/screens/StarterPack/StarterPackScreen.tsx:592 msgid "Edit starter pack" msgstr "" @@ -4670,7 +4670,7 @@ msgstr "" msgid "Enters full screen" msgstr "" -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:244 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:248 msgid "Entertainment" msgstr "" @@ -4942,7 +4942,7 @@ msgstr "" msgid "Failed to delete post, please try again" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:747 +#: src/screens/StarterPack/StarterPackScreen.tsx:750 msgid "Failed to delete starter pack" msgstr "" @@ -5193,10 +5193,10 @@ msgstr "" msgid "Failed to update settings" msgstr "" -#: src/lib/media/video/upload.ts:104 -#: src/lib/media/video/upload.web.ts:104 +#: src/lib/media/video/upload.ts:108 #: src/lib/media/video/upload.web.ts:108 -#: src/lib/media/video/upload.web.ts:118 +#: src/lib/media/video/upload.web.ts:112 +#: src/lib/media/video/upload.web.ts:122 msgid "Failed to upload video" msgstr "" @@ -5265,7 +5265,7 @@ msgstr "" #: src/screens/SavedFeeds.tsx:112 #: src/screens/SavedFeeds.tsx:303 #: src/screens/Search/SearchResults.tsx:113 -#: src/screens/StarterPack/StarterPackScreen.tsx:193 +#: src/screens/StarterPack/StarterPackScreen.tsx:196 #: src/view/screens/Feeds.tsx:504 #: src/view/screens/Profile.tsx:240 #: src/view/shell/desktop/LeftNav.tsx:712 @@ -5482,8 +5482,8 @@ msgstr "" #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:165 #: src/screens/Settings/FindContactsSettings.tsx:469 #: src/screens/Settings/FindContactsSettings.tsx:479 -#: src/screens/StarterPack/StarterPackScreen.tsx:445 -#: src/screens/StarterPack/StarterPackScreen.tsx:453 +#: src/screens/StarterPack/StarterPackScreen.tsx:448 +#: src/screens/StarterPack/StarterPackScreen.tsx:456 msgid "Follow all" msgstr "" @@ -5846,7 +5846,7 @@ msgstr "" #: src/screens/List/ListHiddenScreen.tsx:228 #: src/screens/Profile/ErrorState.tsx:63 #: src/screens/Profile/ErrorState.tsx:67 -#: src/screens/StarterPack/StarterPackScreen.tsx:800 +#: src/screens/StarterPack/StarterPackScreen.tsx:803 msgid "Go Back" msgstr "" @@ -6162,7 +6162,6 @@ msgstr "" msgid "Hidden list" msgstr "" -#: src/components/interstitials/Trending.tsx:149 #: src/components/interstitials/TrendingVideos.tsx:140 #: src/components/moderation/ContentHider.tsx:217 #: src/components/moderation/LabelPreference.tsx:141 @@ -6236,15 +6235,10 @@ msgstr "" msgid "Hide translation" msgstr "Hide translation" -#: src/components/interstitials/Trending.tsx:131 #: src/components/TrendingTopics.tsx:42 msgid "Hide trending topics" msgstr "" -#: src/components/interstitials/Trending.tsx:147 -msgid "Hide trending topics?" -msgstr "" - #: src/components/interstitials/TrendingVideos.tsx:138 msgid "Hide trending videos?" msgstr "" @@ -6807,8 +6801,8 @@ msgstr "Join" #: src/screens/StarterPack/StarterPackLandingScreen.tsx:202 #: src/screens/StarterPack/StarterPackLandingScreen.tsx:207 -#: src/screens/StarterPack/StarterPackScreen.tsx:471 -#: src/screens/StarterPack/StarterPackScreen.tsx:481 +#: src/screens/StarterPack/StarterPackScreen.tsx:474 +#: src/screens/StarterPack/StarterPackScreen.tsx:484 msgid "Join Bluesky" msgstr "" @@ -8067,7 +8061,7 @@ msgid "Newest replies first" msgstr "" #: src/lib/interests.ts:67 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:246 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:250 msgid "News" msgstr "" @@ -8734,7 +8728,7 @@ msgstr "Open settings" msgid "Open share menu" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:575 +#: src/screens/StarterPack/StarterPackScreen.tsx:578 msgid "Open starter pack menu" msgstr "" @@ -9011,7 +9005,7 @@ msgstr "" #: src/screens/ProfileList/index.tsx:164 #: src/screens/Search/SearchResults.tsx:107 -#: src/screens/StarterPack/StarterPackScreen.tsx:192 +#: src/screens/StarterPack/StarterPackScreen.tsx:195 msgid "People" msgstr "" @@ -9350,7 +9344,7 @@ msgid "Please write your message below:" msgstr "" #: src/lib/interests.ts:70 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:240 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:244 msgid "Politics" msgstr "" @@ -9478,7 +9472,7 @@ msgstr "" #: src/components/activity-notifications/SubscribeProfileDialog.tsx:266 #: src/screens/ProfileList/index.tsx:164 #: src/screens/Settings/NotificationSettings/ActivityNotificationSettings.tsx:216 -#: src/screens/StarterPack/StarterPackScreen.tsx:194 +#: src/screens/StarterPack/StarterPackScreen.tsx:197 #: src/view/screens/Profile.tsx:235 msgid "Posts" msgstr "" @@ -10216,8 +10210,8 @@ msgstr "" msgid "Report sent" msgstr "Report sent" -#: src/screens/StarterPack/StarterPackScreen.tsx:650 #: src/screens/StarterPack/StarterPackScreen.tsx:653 +#: src/screens/StarterPack/StarterPackScreen.tsx:656 msgid "Report starter pack" msgstr "" @@ -10283,7 +10277,7 @@ msgstr "" #: src/components/PostControls/RepostButton.tsx:146 #: src/components/PostControls/RepostButton.web.tsx:43 #: src/components/PostControls/RepostButton.web.tsx:103 -#: src/screens/StarterPack/StarterPackScreen.tsx:570 +#: src/screens/StarterPack/StarterPackScreen.tsx:573 msgid "Repost or quote post" msgstr "" @@ -10502,7 +10496,7 @@ msgstr "" #: src/components/Error.tsx:68 #: src/screens/List/ListHiddenScreen.tsx:223 -#: src/screens/StarterPack/StarterPackScreen.tsx:794 +#: src/screens/StarterPack/StarterPackScreen.tsx:797 msgid "Return to previous page" msgstr "" @@ -10827,7 +10821,7 @@ msgstr "" #: src/components/FeedInterstitials.tsx:495 #: src/components/FeedInterstitials.tsx:554 #: src/components/interstitials/FeedTrendingTopics.tsx:116 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:83 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:82 msgid "See more" msgstr "" @@ -10836,7 +10830,7 @@ msgid "See more suggested profiles" msgstr "" #: src/components/interstitials/FeedTrendingTopics.tsx:102 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:69 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:68 msgid "See more trending topics" msgstr "See more trending topics" @@ -11233,7 +11227,7 @@ msgstr "" #: src/screens/Hashtag.tsx:132 #: src/screens/Messages/components/InviteLinkDialog.tsx:412 #: src/screens/Messages/components/InviteLinkDialog.tsx:423 -#: src/screens/StarterPack/StarterPackScreen.tsx:440 +#: src/screens/StarterPack/StarterPackScreen.tsx:443 #: src/screens/Topic.tsx:91 msgid "Share" msgstr "" @@ -11309,7 +11303,7 @@ msgstr "" msgid "Share this search" msgstr "Share this search" -#: src/screens/StarterPack/StarterPackScreen.tsx:433 +#: src/screens/StarterPack/StarterPackScreen.tsx:436 msgid "Share this starter pack" msgstr "" @@ -11321,8 +11315,8 @@ msgstr "" #: src/components/PostControls/ShareMenu/ShareMenuItems.tsx:133 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:161 #: src/screens/ProfileList/components/MoreOptionsMenu.tsx:167 -#: src/screens/StarterPack/StarterPackScreen.tsx:631 -#: src/screens/StarterPack/StarterPackScreen.tsx:639 +#: src/screens/StarterPack/StarterPackScreen.tsx:634 +#: src/screens/StarterPack/StarterPackScreen.tsx:642 #: src/view/com/profile/ProfileMenu.tsx:321 #: src/view/com/profile/ProfileMenu.tsx:333 msgid "Share via..." @@ -11781,7 +11775,7 @@ msgid "Spam or other inauthentic behavior or deception" msgstr "" #: src/lib/interests.ts:72 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:238 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:242 msgid "Sports" msgstr "" @@ -11837,7 +11831,7 @@ msgstr "" msgid "Starter pack by you" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:758 +#: src/screens/StarterPack/StarterPackScreen.tsx:761 msgid "Starter pack is invalid" msgstr "" @@ -12239,8 +12233,8 @@ msgstr "" #: src/screens/StarterPack/StarterPackScreen.tsx:112 #: src/screens/StarterPack/StarterPackScreen.tsx:113 -#: src/screens/StarterPack/StarterPackScreen.tsx:157 -#: src/screens/StarterPack/StarterPackScreen.tsx:158 +#: src/screens/StarterPack/StarterPackScreen.tsx:160 +#: src/screens/StarterPack/StarterPackScreen.tsx:161 #: src/screens/StarterPack/Wizard/index.tsx:112 #: src/screens/StarterPack/Wizard/index.tsx:122 msgid "That starter pack could not be found." @@ -12377,7 +12371,7 @@ msgstr "The selected video uses an unsupported format." msgid "The server appears to be experiencing issues. Please try again in a few moments." msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:768 +#: src/screens/StarterPack/StarterPackScreen.tsx:771 msgid "The starter pack that you are trying to view is invalid. You may delete this starter pack instead." msgstr "" @@ -12453,7 +12447,7 @@ msgid "There was an issue fetching notifications. Tap here to try again." msgstr "" #: src/screens/Search/Explore.tsx:1011 -#: src/view/com/posts/PostFeed.tsx:830 +#: src/view/com/posts/PostFeed.tsx:825 msgid "There was an issue fetching posts. Tap here to try again." msgstr "" @@ -13080,8 +13074,8 @@ msgid "Tree view" msgstr "" #: src/components/interstitials/FeedTrendingTopics.tsx:98 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:72 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:66 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:76 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:65 msgid "Trending" msgstr "" @@ -13091,8 +13085,8 @@ msgid "Trending GIFs" msgstr "Trending GIFs" #: src/components/interstitials/FeedTrendingTopics.tsx:125 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:75 -#: src/view/shell/desktop/SidebarTrendingTopics.tsx:93 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:79 +#: src/view/shell/desktop/SidebarTrendingTopics.tsx:92 msgid "Trending options" msgstr "" @@ -13186,7 +13180,7 @@ msgstr "" msgid "Unable to contact your service. Please check your Internet connection." msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:693 +#: src/screens/StarterPack/StarterPackScreen.tsx:696 msgid "Unable to delete" msgstr "" @@ -13838,7 +13832,7 @@ msgid "Video from {0}. Tap to play or pause the video" msgstr "" #: src/lib/interests.ts:62 -#: src/screens/Search/modules/ExploreTrendingTopics.tsx:242 +#: src/screens/Search/modules/ExploreTrendingTopics.tsx:246 msgid "Video Games" msgstr "" @@ -14494,7 +14488,7 @@ msgstr "" msgid "Yes, delete my account" msgstr "" -#: src/screens/StarterPack/StarterPackScreen.tsx:705 +#: src/screens/StarterPack/StarterPackScreen.tsx:708 msgid "Yes, delete this starter pack" msgstr "" @@ -14662,7 +14656,6 @@ msgstr "You can read chat history but can’t send new messages." msgid "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total." msgstr "You can select up to {MAX_GALLERY_IMAGES, plural, other {# images}} in total." -#: src/components/interstitials/Trending.tsx:148 #: src/components/interstitials/TrendingVideos.tsx:139 #: src/components/TrendingTopics.tsx:53 msgid "You can update this later from your settings." @@ -14775,7 +14768,7 @@ msgstr "" msgid "You have successfully verified your email address. You can close this dialog." msgstr "" -#: src/lib/media/video/upload.shared.ts:74 +#: src/lib/media/video/upload.shared.ts:110 msgid "You have temporarily reached the limit for video uploads. Please try again later." msgstr "" From f87fdd2ea203ba39eaa777b819e62caad9350c7e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 25 Aug 2026 09:40:14 +0300 Subject: [PATCH 08/22] Enable React Native strict TypeScript API (#11459) --- jest/jestSetup.js | 10 + oxlint-suppressions.json | 177 +----------------- src/Navigation.tsx | 4 +- src/Splash.tsx | 11 +- src/Splash.web.tsx | 4 +- .../components/RedirectOverlay.tsx | 1 - src/alf/fonts.ts | 7 +- src/alf/typography.tsx | 10 +- src/alf/util/dimensions.ts | 11 +- src/alf/util/flatten.ts | 19 +- src/alf/util/useColorModeTheme.ts | 5 +- src/analytics/utils.ts | 2 +- src/components/AltBadgeWithDialog.tsx | 6 +- src/components/BetaBadge.tsx | 3 +- src/components/Button.tsx | 5 +- src/components/Composer/index.tsx | 19 +- src/components/ContextMenu/index.tsx | 7 +- src/components/Dialog/index.tsx | 147 ++++++++------- src/components/Dialog/index.web.tsx | 5 +- src/components/FeedInterstitials.tsx | 4 +- src/components/FocusScope/index.tsx | 19 +- src/components/GlassView.tsx | 2 +- src/components/InterestTabs.tsx | 2 +- src/components/Layout/Header/index.tsx | 2 +- src/components/Lightbox/Lightbox.web.tsx | 3 - src/components/Lightbox/chrome/ImageMenu.tsx | 2 +- .../pager/ImageItem/ImageItem.ios.tsx | 4 +- src/components/Lightbox/pager/ImagePager.tsx | 9 +- src/components/Link.tsx | 4 +- src/components/Menu/index.web.tsx | 4 +- src/components/Menu/types.ts | 2 +- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 2 +- src/components/Post/Translated/index.tsx | 2 +- .../PostControls/PostControlButton.tsx | 2 +- src/components/Pressable.tsx | 37 ++++ src/components/ProfileHoverCard/index.web.tsx | 3 +- src/components/ProgressGuide/FollowDialog.tsx | 8 +- src/components/RichText.tsx | 9 +- src/components/Select/index.tsx | 11 +- src/components/Select/index.web.tsx | 18 +- .../StarterPack/Main/ProfilesList.tsx | 7 - src/components/Tooltip/index.tsx | 9 +- src/components/Tooltip/index.web.tsx | 4 +- .../contacts/components/OTPInput.tsx | 4 +- .../contacts/screens/ViewMatches.tsx | 2 +- .../dialogs/SearchablePeopleList.tsx | 4 +- .../lists/UserAddRemoveListsDialog.tsx | 4 +- src/components/dialogs/nuxs/index.tsx | 2 +- src/components/dms/AddMembersFlow.tsx | 4 +- src/components/dms/InitiateChatFlow.tsx | 2 +- src/components/dms/ReactionsDialog.tsx | 2 +- .../dms/components/UserSearchInput.tsx | 3 +- src/components/forms/AutosizedTextarea.tsx | 6 +- .../forms/DateField/index.android.tsx | 1 - src/components/forms/DateField/index.web.tsx | 2 +- src/components/forms/SearchInput.tsx | 4 +- src/components/forms/TextField.tsx | 72 +++---- src/components/hooks/useOnKeyboard.ts | 4 +- src/components/icons/TEMPLATE.tsx | 2 +- src/components/images/Gallery/index.tsx | 12 +- src/components/moderation/BlockDialog.tsx | 2 +- .../moderation/ReportDialog/index.tsx | 2 +- src/features/gifPicker/GifPickerDialog.tsx | 2 +- .../gifPicker/components/GifPickerGrid.tsx | 2 +- .../gifPicker/components/GifPickerHeader.tsx | 2 +- src/features/liveEvents/preferences.ts | 2 +- src/lib/api/feed/merge.ts | 2 +- src/lib/batchedUpdates.ts | 6 +- src/lib/hooks/useDraggableScrollView.ts | 14 +- src/lib/hooks/useOTAUpdates.test.ts | 6 +- src/lib/hooks/useOTAUpdates.ts | 2 +- src/lib/jwt.ts | 2 +- src/lib/media/picker.shared.ts | 2 + src/lib/strings/embed-player.ts | 8 +- src/locale/deviceLocales.ts | 2 +- src/logger/__tests__/logger.test.ts | 6 +- src/platform/markBundleStartTime.ts | 2 +- src/screens/Hashtag.tsx | 1 - src/screens/Login/LoginForm.tsx | 4 +- src/screens/Messages/JoinRequests.tsx | 2 +- .../Messages/components/MessageComposer.tsx | 6 +- src/screens/Onboarding/Layout.tsx | 2 +- .../StepFinished/ValuePropositionPager.tsx | 3 +- .../StepProfile/PlaceholderCanvas.tsx | 1 - .../StepSuggestedAccounts/index.tsx | 4 +- src/screens/PostThread/index.tsx | 4 +- src/screens/Profile/KnownFollowers.tsx | 1 - src/screens/Search/Explore.tsx | 3 +- src/screens/Search/Shell.tsx | 2 +- .../AdvancedSearchDialog/ClearableInput.tsx | 2 +- .../components/AdvancedSearchDialog/index.tsx | 4 +- .../SearchAutocompleteInput/index.tsx | 2 +- src/screens/Settings/AppIconSettings/types.ts | 6 +- .../ActivityNotificationSettings.tsx | 2 +- .../components/DeleteAccountDialog.tsx | 2 +- .../Signup/StepCaptcha/CaptchaWebView.web.tsx | 3 - src/screens/Signup/StepInfo/index.tsx | 4 +- src/screens/StarterPack/StarterPackScreen.tsx | 14 +- src/screens/Topic.tsx | 1 - src/screens/VideoFeed/index.tsx | 8 +- src/state/cache/thread-mutes.tsx | 1 - src/state/persisted/schema.ts | 2 +- src/state/queries/nuxs/index.ts | 2 +- src/state/queries/usePostThread/utils.ts | 6 +- src/state/queries/usePostThread/views.ts | 4 +- src/storage/__tests__/index.test.ts | 2 +- src/view/com/auth/SplashScreen.tsx | 8 +- src/view/com/auth/SplashScreen.web.tsx | 1 - src/view/com/composer/drafts/DraftItem.tsx | 3 +- src/view/com/composer/photos/Gallery.tsx | 2 +- .../com/composer/text-input/TextInput.tsx | 10 +- .../com/composer/text-input/TextInput.web.tsx | 5 +- .../composer/text-input/web/Autocomplete.tsx | 4 +- .../videos/VideoTranscodeBackdrop.web.tsx | 4 +- src/view/com/feeds/ComposerPrompt.tsx | 1 - src/view/com/pager/DraggableScrollView.tsx | 2 +- src/view/com/pager/PagerWithHeader.tsx | 8 +- src/view/com/pager/PagerWithHeader.web.tsx | 6 +- src/view/com/pager/TabBar.web.tsx | 5 +- src/view/com/post-thread/PostQuotes.tsx | 1 - src/view/com/post-thread/PostRepostedBy.tsx | 1 - src/view/com/post/Post.tsx | 1 - src/view/com/profile/ProfileFollowers.tsx | 1 - src/view/com/profile/ProfileFollows.tsx | 1 - src/view/com/util/Alert.web.tsx | 4 +- src/view/com/util/EventStopper.tsx | 1 - src/view/com/util/Link.tsx | 20 +- src/view/com/util/List.tsx | 3 +- src/view/com/util/List.web.tsx | 11 +- src/view/com/util/PressableWithHover.tsx | 25 +-- src/view/com/util/Views.tsx | 12 +- src/view/com/util/Views.web.tsx | 10 +- src/view/com/util/WebAuxClickWrapper.tsx | 2 +- src/view/com/util/fab/FABInner.tsx | 2 +- src/view/com/util/forms/Button.tsx | 15 +- src/view/com/util/layouts/LoggedOutLayout.tsx | 1 - src/view/com/util/listNativeTag.ts | 2 +- src/view/com/util/text/Text.tsx | 16 +- src/view/icons/Logo.tsx | 4 +- src/view/icons/Logomark.tsx | 2 +- src/view/icons/Logotype.tsx | 2 +- src/view/screens/DebugMod.tsx | 4 +- src/view/screens/Home.tsx | 2 +- src/view/screens/Notifications.tsx | 2 +- src/view/screens/Profile.tsx | 2 +- src/view/screens/Storybook/Forms.tsx | 2 +- src/view/shell/Composer.web.tsx | 2 - src/view/shell/desktop/LeftNav.tsx | 2 +- tsconfig.json | 3 +- 149 files changed, 506 insertions(+), 623 deletions(-) create mode 100644 src/components/Pressable.tsx 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..1ef59ea20c 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 } @@ -845,15 +768,9 @@ }, "typescript/no-floating-promises": { "count": 1 - }, - "typescript/no-misused-promises": { - "count": 1 } }, "src/screens/E2E/SharedPreferencesTesterScreen.tsx": { - "typescript/no-misused-promises": { - "count": 6 - }, "typescript/require-await": { "count": 6 } @@ -863,22 +780,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 +798,7 @@ "count": 1 }, "typescript/no-misused-promises": { - "count": 2 + "count": 1 }, "typescript/no-unsafe-member-access": { "count": 2 @@ -922,19 +826,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 +853,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 +889,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 +907,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 @@ -1093,9 +968,6 @@ "src/screens/Settings/components/ChangePasswordDialog.tsx": { "typescript/no-explicit-any": { "count": 2 - }, - "typescript/no-misused-promises": { - "count": 2 } }, "src/screens/Settings/components/CopyButton.tsx": { @@ -1107,9 +979,6 @@ "typescript/no-explicit-any": { "count": 1 }, - "typescript/no-misused-promises": { - "count": 1 - }, "typescript/no-unsafe-member-access": { "count": 1 } @@ -1119,11 +988,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 +1011,7 @@ "count": 1 }, "typescript/no-misused-promises": { - "count": 2 + "count": 1 }, "typescript/no-unsafe-call": { "count": 1 @@ -1168,9 +1032,6 @@ "typescript/no-floating-promises": { "count": 1 }, - "typescript/no-misused-promises": { - "count": 2 - }, "typescript/require-await": { "count": 1 } @@ -1474,9 +1335,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 +1358,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 +1366,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 +1410,7 @@ "count": 2 }, "typescript/no-misused-promises": { - "count": 3 + "count": 2 }, "typescript/no-unsafe-member-access": { "count": 6 @@ -1598,12 +1448,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 +1471,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 +1534,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 +1541,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 diff --git a/src/Navigation.tsx b/src/Navigation.tsx index ec3072f46d..daa76629d1 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1006,7 +1006,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { ax.metric('init', { initMs: Math.round( - // @ts-ignore Emitted by Metro in the bundle prelude + // @ts-expect-error Emitted by Metro in the bundle prelude performance.now() - global.__BUNDLE_START_TIME__, ), }) @@ -1081,7 +1081,7 @@ function navigate( } navigationRef.addListener('state', handler) - // @ts-ignore I don't know what would make typescript happy but I have a life -prf + // @ts-expect-error I don't know what would make typescript happy but I have a life -prf navigationRef.navigate(name, params) }), timeout(1e3), diff --git a/src/Splash.tsx b/src/Splash.tsx index 4a4dea831a..76c9c7de82 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -20,14 +20,15 @@ import * as SplashScreen from 'expo-splash-screen' import {Logotype} from '#/view/icons/Logotype' import {atoms as a} from '#/alf' -// @ts-ignore +// @ts-expect-error import splashImagePointer from '../assets/splash/splash.png' -// @ts-ignore +// @ts-expect-error 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 @@ -35,7 +36,7 @@ export const Logo = forwardRef(function LogoImpl(props: SvgProps, ref) { return ( 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..dfcc57b53a 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, ) { @@ -39,11 +39,8 @@ function getThemeName( } function updateDocument(theme: ThemeName) { - // @ts-ignore web only if (IS_WEB && typeof window !== 'undefined') { - // @ts-ignore web only const html = window.document.documentElement - // @ts-ignore web only const meta = window.document.querySelector('meta[name="theme-color"]') // remove any other color mode classes diff --git a/src/analytics/utils.ts b/src/analytics/utils.ts index 95d2efb589..b20b3719c9 100644 --- a/src/analytics/utils.ts +++ b/src/analytics/utils.ts @@ -14,7 +14,7 @@ import { export function useMeta(metadata?: MergeableMetadata) { const m = useMemo(() => metadata, [metadata]) if (!m) return - // @ts-ignore + // @ts-expect-error m.__meta = true return m } 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..2c1f7ff3f8 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, @@ -575,7 +577,6 @@ export const Button = forwardRef( role="button" accessibilityHint={undefined} // optional {...rest} - // @ts-ignore - this will always be a pressable ref={ref} aria-label={label} aria-pressed={state.pressed} diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index f4a0302ff7..b2266db5a1 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. @@ -306,7 +314,7 @@ export function Composer({ style={[a.absolute, a.inset_0, a.z_10, {overflow: 'hidden'}]} ref={node => { if (IS_WEB && node) { - // @ts-ignore web only a11y + // @ts-expect-error web only a11y node.setAttribute('inert', '') } }}> @@ -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) @@ -359,11 +367,10 @@ export function Composer({ inputScrollSharedValue.value = e.nativeEvent.contentOffset.y } }} - // @ts-ignore web only + // @ts-expect-error web only onCompositionStart={() => { isComposing.current = true }} - // @ts-ignore web only onCompositionEnd={() => { isComposing.current = false }} 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..59bbe85752 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' @@ -276,7 +276,7 @@ export function ProfileGrid({ }, {threshold: 0.5}, ) - // @ts-ignore - web only + // @ts-expect-error - web only observer.observe(node) return () => observer.disconnect() } else { 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/Lightbox.web.tsx b/src/components/Lightbox/Lightbox.web.tsx index 0222aa3156..cccbe4f851 100644 --- a/src/components/Lightbox/Lightbox.web.tsx +++ b/src/components/Lightbox/Lightbox.web.tsx @@ -446,9 +446,7 @@ function LightboxGalleryItem({ const styles = StyleSheet.create({ avi: { - // @ts-ignore web-only maxWidth: `calc(min(400px, 100vw))`, - // @ts-ignore web-only maxHeight: `calc(min(400px, 100vh))`, padding: 16, boxSizing: 'border-box', @@ -458,7 +456,6 @@ const styles = StyleSheet.create({ // column via ScrollView's default flexGrow. flexGrow: 0, flexShrink: 0, - // @ts-ignore web-only -sfn maxHeight: '50vh', }, menuBtn: { 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..c4a842a74e 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) } @@ -109,7 +109,6 @@ const ImageItem = ({ height: number }) { const scrollResponderRef = scrollViewRef?.current?.getScrollResponder() - // @ts-ignore scrollResponderRef?.scrollResponderZoomTo({ ...nextZoomRect, // This rect is in screen coordinates animated: true, @@ -218,7 +217,6 @@ const ImageItem = ({ return ( { + 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/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx index d20cc0d90a..947d4ac3d6 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -149,7 +149,7 @@ function canPlayBskyVideoCodecs(): boolean { type CachedPromise = Promise & {value: undefined | T} const promiseForHls = import( - // @ts-ignore + // @ts-expect-error 'hls.js/dist/hls.min' // oxlint-disable-next-line typescript/no-unsafe-member-access ).then(mod => mod.default) as CachedPromise 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..5fc96e3047 --- /dev/null +++ b/src/components/Pressable.tsx @@ -0,0 +1,37 @@ +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/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx index ccc48b8951..a5f47be28b 100644 --- a/src/components/ProfileHoverCard/index.web.tsx +++ b/src/components/ProfileHoverCard/index.web.tsx @@ -318,11 +318,10 @@ export function ProfileHoverCardInner(props: ProfileHoverCardProps) { return ( {props.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..1fa020f2a9 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 ( @@ -104,7 +104,6 @@ export function RichText({ style={[plainStyles, {fontSize}, suffixStyles]} onLayout={onLayout} onTextLayout={onTextLayout} - // @ts-ignore web only -prf dataSet={WORD_WRAP}> {text} {suffix ? ' ' : null} @@ -121,7 +120,6 @@ export function RichText({ numberOfLines={numberOfLines} onLayout={onLayout} onTextLayout={onTextLayout} - // @ts-ignore web only -prf dataSet={WORD_WRAP}> {text} {suffix ? ' ' : null} @@ -150,7 +148,7 @@ export function RichText({ selectable={selectable} to={`/profile/${mention.did}`} style={interactiveStyles} - // @ts-ignore TODO + // @ts-expect-error TODO dataSet={WORD_WRAP} shouldProxy={shouldProxyLinks} onPress={onLinkPress}> @@ -169,7 +167,7 @@ export function RichText({ key={key} to={link.uri} style={interactiveStyles} - // @ts-ignore TODO + // @ts-expect-error TODO dataSet={WORD_WRAP} shareOnLongPress shouldProxy={shouldProxyLinks} @@ -209,7 +207,6 @@ export function RichText({ numberOfLines={numberOfLines} onLayout={onLayout} onTextLayout={onTextLayout} - // @ts-ignore web only -prf dataSet={WORD_WRAP}> {els} {suffix ? ' ' : null} 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 (