diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch index 5d2900a7..789ba84a 100644 --- a/patches/react-native+0.74.1.patch +++ b/patches/react-native+0.74.1.patch @@ -1,3 +1,29 @@ +diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +index b0d71dc..9974932 100644 +--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm ++++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing + self.backedTextInputView.attributedText = [NSAttributedString new]; + } + +- if (_selectTextOnFocus) { +- [self.backedTextInputView selectAll:nil]; +- } +- + [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus + reactTag:self.reactTag + text:[self.backedTextInputView.attributedText.string copy] +@@ -611,6 +607,10 @@ - (UIView *)reactAccessibilityElement + - (void)reactFocus + { + [self.backedTextInputView reactFocus]; ++ ++ if (_selectTextOnFocus) { ++ [self.backedTextInputView selectAll:nil]; ++ } + } + + - (void)reactBlur diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h index e9b330f..1ecdf0a 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h @@ -5,7 +31,7 @@ index e9b330f..1ecdf0a 100644 @@ -16,4 +16,6 @@ @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, weak) UIScrollView *scrollView; - + +- (void)forwarderBeginRefreshing; + @end @@ -16,7 +42,7 @@ index b09e653..4c32b31 100644 @@ -198,9 +198,53 @@ - (void)refreshControlValueChanged [self setCurrentRefreshingState:super.refreshing]; _refreshingProgrammatically = NO; - + + if (@available(iOS 17.4, *)) { + if (_currentRefreshingState) { + UIImpactFeedbackGenerator *feedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; @@ -29,7 +55,7 @@ index b09e653..4c32b31 100644 _onRefresh(nil); } } - + +/* + This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native + libraries to perform a refresh of a scrollview and access the refresh control's onRefresh @@ -38,15 +64,15 @@ index b09e653..4c32b31 100644 +- (void)forwarderBeginRefreshing +{ + _refreshingProgrammatically = NO; -+ ++ + [self sizeToFit]; -+ ++ + if (!self.scrollView) { + return; + } -+ ++ + UIScrollView *scrollView = (UIScrollView *)self.scrollView; -+ ++ + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState @@ -58,7 +84,7 @@ index b09e653..4c32b31 100644 + completion:^(__unused BOOL finished) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; -+ ++ + if (self->_onRefresh) { + self->_onRefresh(nil); + } @@ -73,7 +99,7 @@ index 5f5e1ab..aac00b6 100644 +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java @@ -99,8 +99,9 @@ public class JavaTimerManager { } - + // If the JS thread is busy for multiple frames we cancel any other pending runnable. - if (mCurrentIdleCallbackRunnable != null) { - mCurrentIdleCallbackRunnable.cancel(); @@ -81,5 +107,5 @@ index 5f5e1ab..aac00b6 100644 + if (currentRunnable != null) { + currentRunnable.cancel(); } - + mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos); diff --git a/patches/react-native+0.74.1.patch.md b/patches/react-native+0.74.1.patch.md index 9c93aee5..84953df2 100644 --- a/patches/react-native+0.74.1.patch.md +++ b/patches/react-native+0.74.1.patch.md @@ -11,3 +11,10 @@ in the RN repo: https://github.com/facebook/react-native/issues/43388 Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarderBeginRefreshing` method to the class. This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that module. + + +## TextInput Patch - `selectTextOnFocus` fix + +Patching `RCTBaseTextInputView.m` to fix an issue where `selectTextOnFocus` does not work as expected on iOS 17. This +patch _only_ fixes the Paper version of `TextInput`. If we migrate to Fabric and the fix has not been made upstream, +we can apply the same fix. See https://github.com/facebook/react-native/pull/44307. diff --git a/src/App.native.tsx b/src/App.native.tsx index 5b2071e1..322e944a 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -14,7 +14,11 @@ import * as SplashScreen from 'expo-splash-screen' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {Provider as StatsigProvider} from '#/lib/statsig/statsig' +import { + initialize, + Provider as StatsigProvider, + tryFetchGates, +} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {MessagesProvider} from '#/state/messages' import {init as initPersistedState} from '#/state/persisted' @@ -69,6 +73,9 @@ function InnerApp() { try { if (account) { await resumeSession(account) + } else { + await initialize() + await tryFetchGates(undefined, 'prefer-fresh-gates') } } catch (e) { logger.error(`session: resume failed`, {message: e}) diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index 2745ed7c..94d97cb6 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -11,6 +11,7 @@ import { useRemoveFeedMutation, } from '#/state/queries/preferences' import {sanitizeHandle} from 'lib/strings/handles' +import {useSession} from 'state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import * as Toast from 'view/com/util/Toast' import {useTheme} from '#/alf' @@ -116,6 +117,12 @@ export function Likes({count}: {count: number}) { } export function Action({uri, pin}: {uri: string; pin?: boolean}) { + const {hasSession} = useSession() + if (!hasSession) return null + return +} + +function ActionInner({uri, pin}: {uri: string; pin?: boolean}) { const {_} = useLingui() const {data: preferences} = usePreferencesQuery() const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} = diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 4481935f..6e460dc6 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,5 +1,6 @@ export type Gate = // Keep this alphabetic please. + | 'native_pwi_disabled' | 'request_notifications_permission_after_onboarding_v2' | 'show_avi_follow_button' | 'show_follow_back_label_v2' diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index f6aed999..b5a239c3 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -14,6 +14,8 @@ import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback' import {LogEvents} from './events' import {Gate} from './gates' +const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV' + type StatsigUser = { userID: string | undefined // TODO: Remove when enough users have custom.platform: @@ -251,7 +253,7 @@ AppState.addEventListener('change', (state: AppStateStatus) => { }) export async function tryFetchGates( - did: string, + did: string | undefined, strategy: 'prefer-low-latency' | 'prefer-fresh-gates', ) { try { @@ -275,6 +277,10 @@ export async function tryFetchGates( } } +export function initialize() { + return Statsig.initialize(SDK_KEY, null, createStatsigOptions([])) +} + export function Provider({children}: {children: React.ReactNode}) { const {currentAccount, accounts} = useSession() const did = currentAccount?.did @@ -320,7 +326,7 @@ export function Provider({children}: {children: React.ReactNode}) { ) { {children} - + diff --git a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx index 29ba39a0..d1d1af6d 100644 --- a/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx +++ b/src/screens/Onboarding/StepProfile/PlaceholderCanvas.tsx @@ -5,7 +5,7 @@ import ViewShot from 'react-native-view-shot' import {useAvatar} from '#/screens/Onboarding/StepProfile/index' import {atoms as a} from '#/alf' -const SIZE_MULTIPLIER = 1.5 +const SIZE_MULTIPLIER = 5 export interface PlaceholderCanvasRef { capture: () => Promise diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 2981b41b..83d6a763 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -234,26 +234,28 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { data: InfiniteData, ) => { const {savedFeeds, hasSession: hasSessionInner} = selectArgs - data?.pages.map(page => { - page.feeds = page.feeds.filter(feed => { - if ( - !hasSessionInner && - KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) - ) { - return false - } - const alreadySaved = Boolean( - savedFeeds?.find(f => { - return f.value === feed.uri + return { + ...data, + pages: data.pages.map(page => { + return { + ...page, + feeds: page.feeds.filter(feed => { + if ( + !hasSessionInner && + KNOWN_AUTHED_ONLY_FEEDS.includes(feed.uri) + ) { + return false + } + const alreadySaved = Boolean( + savedFeeds?.find(f => { + return f.value === feed.uri + }), + ) + return !alreadySaved }), - ) - return !alreadySaved - }) - - return page - }) - - return data + } + }), + } }, [selectArgs /* Don't change. Everything needs to go into selectArgs. */], ), diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index f6e99883..c7f5f939 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -16,18 +16,17 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' -import {useSession} from '#/state/session' import {cleanError} from 'lib/strings/errors' import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard' import {List} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' import { FeedFeedLoadingPlaceholder, ProfileCardFeedLoadingPlaceholder, } from 'view/com/util/LoadingPlaceholder' import {atoms as a, useTheme, ViewStyleProp} from '#/alf' import {Button} from '#/components/Button' +import * as FeedCard from '#/components/FeedCard' import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Props as SVGIconProps} from '#/components/icons/common' @@ -271,7 +270,6 @@ type ExploreScreenItems = export function Explore() { const {_} = useLingui() const t = useTheme() - const {hasSession} = useSession() const {data: preferences, error: preferencesError} = usePreferencesQuery() const moderationOpts = useModerationOpts() const { @@ -480,15 +478,14 @@ export function Explore() { } case 'feed': { return ( - - + + ) } @@ -538,7 +535,7 @@ export function Explore() { } } }, - [t, hasSession, moderationOpts], + [t, moderationOpts], ) return ( diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx index ed132d24..0b1fe37a 100644 --- a/src/view/screens/Search/Search.tsx +++ b/src/view/screens/Search/Search.tsx @@ -30,7 +30,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {NavigationProp} from '#/lib/routes/types' import {augmentSearchQuery} from '#/lib/strings/helpers' import {logger} from '#/logger' -import {isIOS, isNative, isWeb} from '#/platform/detection' +import {isNative, isWeb} from '#/platform/detection' import {listenSoftReset} from '#/state/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete' @@ -57,8 +57,8 @@ import {Text} from '#/view/com/util/text/Text' import {CenteredView, ScrollView} from '#/view/com/util/Views' import {Explore} from '#/view/screens/Search/Explore' import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' -import {atoms as a} from '#/alf' +import {atoms as a, useTheme as useThemeNew} from '#/alf' +import * as FeedCard from '#/components/FeedCard' import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu' function Loader() { @@ -285,8 +285,8 @@ let SearchScreenFeedsResults = ({ query: string active: boolean }): React.ReactNode => { + const t = useThemeNew() const {_} = useLingui() - const {hasSession} = useSession() const {data: results, isFetched} = usePopularFeedsSearch({ query, @@ -299,13 +299,15 @@ let SearchScreenFeedsResults = ({ ( - + + + )} keyExtractor={item => item.uri} // @ts-ignore web only -prf @@ -802,12 +804,6 @@ let SearchInputBox = ({ }) } else { setShowAutocomplete(true) - if (isIOS) { - // We rely on selectTextOnFocus, but it's broken on iOS: - // https://github.com/facebook/react-native/issues/41988 - textInput.current?.setSelection(0, searchText.length) - // We still rely on selectTextOnFocus for it to be instant on Android. - } } }} onChangeText={onChangeText} diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 3c611351..82dd6d22 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -29,7 +29,8 @@ import { useLoggedOutView, useLoggedOutViewControls, } from '#/state/shell/logged-out' -import {isWeb} from 'platform/detection' +import {useGate} from 'lib/statsig/statsig' +import {isNative, isWeb} from 'platform/detection' import {Deactivated} from '#/screens/Deactivated' import {Onboarding} from '#/screens/Onboarding' import {SignupQueued} from '#/screens/SignupQueued' @@ -50,6 +51,7 @@ function NativeStackNavigator({ screenOptions, ...rest }: NativeStackNavigatorProps) { + const gate = useGate() // --- this is copy and pasted from the original native stack navigator --- const {state, descriptors, navigation, NavigationContent} = useNavigationBuilder< @@ -100,7 +102,11 @@ function NativeStackNavigator({ const {showLoggedOut} = useLoggedOutView() const {setShowLoggedOut} = useLoggedOutViewControls() const {isMobile, isTabletOrMobile} = useWebMediaQueries() - if ((!PWI_ENABLED || activeRouteRequiresAuth) && !hasSession) { + const isNativePWIDisabled = isNative && gate('native_pwi_disabled') + if ( + (!PWI_ENABLED || isNativePWIDisabled || activeRouteRequiresAuth) && + !hasSession + ) { return } if (hasSession && currentAccount?.signupQueued) {