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) {