React Native New Arch (#10980)

This commit is contained in:
Oleksii Bulenok
2026-07-23 17:25:40 +02:00
committed by GitHub
parent b80d09000f
commit 8d64ab9d4b
72 changed files with 2776 additions and 1596 deletions
-2
View File
@@ -89,14 +89,12 @@ const SPRING_IN: WithSpringConfig = {
mass: 0.75,
damping: 300,
stiffness: 1200,
restDisplacementThreshold: 0.01,
}
const SPRING_OUT: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 150,
stiffness: 1000,
restDisplacementThreshold: 0.01,
}
/**
+11 -10
View File
@@ -3,6 +3,7 @@ import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, {
type AnimatedRef,
measure,
Reanimated3DefaultSpringConfig,
runOnJS,
scrollTo,
type SharedValue,
@@ -237,8 +238,8 @@ function SortableItem<T>({
itemKey: string
itemCount: number
itemHeight: number
state: Animated.SharedValue<DragState>
dragY: Animated.SharedValue<number>
state: SharedValue<DragState>
dragY: SharedValue<number>
scrollCompensation: SharedValue<number>
isGestureActive: SharedValue<boolean>
measureDone: SharedValue<boolean>
@@ -370,18 +371,18 @@ function SortableItem<T>({
return {
transform: [
{translateY: s.dragStartSlot * itemHeight + dragY.get()},
{scale: withSpring(1.03)},
{scale: withSpring(1.03, Reanimated3DefaultSpringConfig)},
],
zIndex: 999,
...(IS_IOS
? {
shadowColor: '#000',
shadowOffset: {width: 0, height: 1},
shadowOpacity: withSpring(0.08),
shadowRadius: withSpring(4),
shadowOpacity: withSpring(0.08, Reanimated3DefaultSpringConfig),
shadowRadius: withSpring(4, Reanimated3DefaultSpringConfig),
}
: {
elevation: withSpring(3),
elevation: withSpring(3, Reanimated3DefaultSpringConfig),
}),
}
}
@@ -391,11 +392,11 @@ function SortableItem<T>({
const inactive = {
...(IS_IOS
? {
shadowOpacity: withSpring(0),
shadowRadius: withSpring(0),
shadowOpacity: withSpring(0, Reanimated3DefaultSpringConfig),
shadowRadius: withSpring(0, Reanimated3DefaultSpringConfig),
}
: {
elevation: withSpring(0),
elevation: withSpring(0, Reanimated3DefaultSpringConfig),
}),
}
@@ -425,7 +426,7 @@ function SortableItem<T>({
return {
transform: [
{translateY: withTiming(baseY + offset, {duration: 200})},
{scale: withSpring(1)},
{scale: withSpring(1, Reanimated3DefaultSpringConfig)},
],
zIndex: 0,
...inactive,
+2
View File
@@ -15,6 +15,8 @@ export const IS_GLASS_AVAILABLE =
* Liquid Glass View that uses `expo-glass-effect`
*
* If unavailable, falls back to a regular `View`. Use `fallbackStyle` to customize the fallback appearance.
* Note: Setting opacity to 0 on Expo GlassView or any of its parent views causes the glass effect to not render at all. https://docs.expo.dev/versions/v56.0.0/sdk/glass-effect/#known-issues
* If animating the opacity of a parent view, start from a non-zero opacity to avoid this issue.
*/
export const GlassView = IS_GLASS_AVAILABLE ? InnerGlassView : FallbackView
+39 -36
View File
@@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
import {atoms as a, tokens, useTheme, web} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {Button, ButtonIcon} from '#/components/Button'
@@ -200,42 +201,44 @@ export function InterestTabs({
return (
<View style={[a.relative, a.flex_row]}>
<DraggableScrollView
ref={listRef}
contentContainerStyle={[
a.gap_sm,
{paddingHorizontal: gutterWidth},
contentContainerStyle,
]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.filter(o => !!o).length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
onContentSizeChange={width => setContentWidth(width)}
onScroll={evt => {
const newScrollX = evt.nativeEvent.contentOffset.x
setScrollX(newScrollX)
}}
scrollEventThrottle={16}>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !disabled
return (
<TabComponent
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</DraggableScrollView>
<BlockDrawerGesture>
<DraggableScrollView
ref={listRef}
contentContainerStyle={[
a.gap_sm,
{paddingHorizontal: gutterWidth},
contentContainerStyle,
]}
showsHorizontalScrollIndicator={false}
decelerationRate="fast"
snapToOffsets={
tabOffsets.filter(o => !!o).length === interests.length
? tabOffsets.map(o => o.x - tokens.space.xl)
: undefined
}
onLayout={evt => setTotalWidth(evt.nativeEvent.layout.width)}
onContentSizeChange={width => setContentWidth(width)}
onScroll={evt => {
const newScrollX = evt.nativeEvent.contentOffset.x
setScrollX(newScrollX)
}}
scrollEventThrottle={16}>
{interests.map((interest, i) => {
const active = interest === selectedInterest && !disabled
return (
<TabComponent
key={interest}
onSelectTab={handleSelectTab}
active={active}
index={i}
interest={interest}
interestsDisplayName={interestsDisplayNames[interest]}
onLayout={handleTabLayout}
/>
)
})}
</DraggableScrollView>
</BlockDrawerGesture>
{IS_WEB && canScrollLeft && (
<View
style={[
+53 -55
View File
@@ -1,4 +1,4 @@
import {forwardRef, memo, useContext, useMemo} from 'react'
import {memo, useContext, useMemo} from 'react'
import {
type StyleProp,
View,
@@ -6,6 +6,7 @@ import {
type ViewStyle,
} from 'react-native'
import Animated, {
type AnimatedRef,
type AnimatedScrollViewProps,
useAnimatedStyle,
} from 'react-native-reanimated'
@@ -73,67 +74,64 @@ export type ContentProps = AnimatedScrollViewProps & {
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
ignoreTabletLayoutOffset?: boolean
ref?: AnimatedRef<Animated.ScrollView>
}
/**
* Default scroll view for simple pages
*/
export const Content = memo(
forwardRef<Animated.ScrollView, ContentProps>(function Content(
{
children,
style,
contentContainerStyle,
ignoreTabletLayoutOffset,
...props
},
ref,
) {
const t = useTheme()
const {footerHeight} = useShellLayout()
const {isWithinSplitView} = useIsWithinSplitView()
export const Content = memo(function Content({
children,
style,
contentContainerStyle,
ignoreTabletLayoutOffset,
ref,
...props
}: ContentProps) {
const t = useTheme()
const {footerHeight} = useShellLayout()
const {isWithinSplitView} = useIsWithinSplitView()
// note - if we ever make the footer transparent in any way,
// we'll need to change this to use contentInsets/scrollIndicatorInsets
// on iOS and contentContainerStyle padding on Android -sfn
const animatedStyle = useAnimatedStyle(() => {
return {
marginBottom: footerHeight.get(),
}
})
// note - if we ever make the footer transparent in any way,
// we'll need to change this to use contentInsets/scrollIndicatorInsets
// on iOS and contentContainerStyle padding on Android -sfn
const animatedStyle = useAnimatedStyle(() => {
return {
marginBottom: footerHeight.get(),
}
})
return (
<Animated.ScrollView
ref={ref}
id="content"
automaticallyAdjustsScrollIndicatorInsets={false}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
style={[
a.w_full,
animatedStyle,
isWithinSplitView &&
web({
flex: 1,
overflowY: 'scroll',
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
}),
style,
]}
contentContainerStyle={[contentContainerStyle]}
{...props}>
{IS_WEB ? (
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
{/* @ts-expect-error web only -esb */}
{children}
</Center>
) : (
children
)}
</Animated.ScrollView>
)
}),
)
return (
<Animated.ScrollView
ref={ref}
id="content"
automaticallyAdjustsScrollIndicatorInsets={false}
indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'}
style={[
a.w_full,
animatedStyle,
isWithinSplitView &&
web({
flex: 1,
overflowY: 'scroll',
scrollbarWidth: 'thin',
scrollbarColor: `${t.palette.contrast_100} transparent`,
}),
style,
]}
contentContainerStyle={[contentContainerStyle]}
{...props}>
{IS_WEB ? (
<Center ignoreTabletLayoutOffset={ignoreTabletLayoutOffset}>
{/* @ts-expect-error web only -esb */}
{children}
</Center>
) : (
children
)}
</Animated.ScrollView>
)
})
/**
* Utility component to center content within the screen
@@ -7,6 +7,7 @@ import {
} from 'react-native-gesture-handler'
import Animated, {
type AnimatableValue,
Reanimated3DefaultSpringConfig,
runOnJS,
type SharedValue,
useAnimatedReaction,
@@ -463,7 +464,10 @@ function clampTranslation(
function withClampedSpring<T extends AnimatableValue>(value: T): T {
'worklet'
return withSpring(value, {overshootClamping: true})
return withSpring(value, {
...Reanimated3DefaultSpringConfig,
overshootClamping: true,
})
}
export default memo(ImageItem)
@@ -170,8 +170,6 @@ const ImageItem = ({
width: screenSize.width,
maxHeight: screenSize.height,
alignSelf: 'center',
aspectRatio: imageAspect ?? 1 /* force onLoad */,
opacity: imageAspect === undefined ? 0 : 1,
}
})
@@ -180,11 +178,19 @@ const ImageItem = ({
return {
transform: cropContentTransform,
width: '100%',
aspectRatio: imageAspect ?? 1 /* force onLoad */,
opacity: imageAspect === undefined ? 0 : 1,
}
})
/*
* When the aspect ratio is unknown until onLoad fires, these layout props
* change after mount. They must be applied via a React render rather than
* useAnimatedStyle
*/
const imageLayoutStyle = {
aspectRatio: imageAspect ?? 1 /* force onLoad */,
opacity: imageAspect === undefined ? 0 : 1,
}
const [showLoader, setShowLoader] = useState(false)
const [hasLoaded, setHasLoaded] = useState(false)
useAnimatedReaction(
@@ -225,8 +231,8 @@ const ImageItem = ({
{showLoader && (
<ActivityIndicator size="small" color="#FFF" style={styles.loading} />
)}
<Animated.View style={imageCropStyle}>
<Animated.View style={imageStyle}>
<Animated.View style={[imageCropStyle, imageLayoutStyle]}>
<Animated.View style={[imageStyle, imageLayoutStyle]}>
<Image
contentFit="contain"
source={{uri: imageSrc.uri}}
+2 -3
View File
@@ -60,13 +60,11 @@ const SLOW_SPRING: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 300,
stiffness: 800,
restDisplacementThreshold: 0.001,
}
const FAST_SPRING: WithSpringConfig = {
mass: IS_IOS ? 1.25 : 0.75,
damping: 150,
stiffness: 900,
restDisplacementThreshold: 0.001,
}
function canAnimate(lightbox: Lightbox): boolean {
@@ -532,7 +530,7 @@ function LightboxImage({
const dismissTranslateY =
isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0
if (openProgressValue === 0 && isFlyingAway.get()) {
if (openProgressValue === 0) {
return {
isHidden: true,
isResting: false,
@@ -609,6 +607,7 @@ function LightboxImage({
return withSpring(0, {
stiffness: 700,
damping: 50,
mass: 1,
reduceMotion: ReduceMotion.Never,
})
})
+1 -2
View File
@@ -6,7 +6,6 @@
*
*/
import {type Component} from 'react'
import {type TransformsStyle} from 'react-native'
import {
type AnimatedRef,
@@ -29,7 +28,7 @@ export type ImageSource = {
thumbUri: string
thumbDimensions: Dimensions | null
thumbRect: MeasuredDimensions | null
thumbRef?: AnimatedRef<Component> | null
thumbRef?: AnimatedRef | null
thumbBorderRadius?: number
alt?: string
type: 'image' | 'circle-avi' | 'rect-avi'
+1
View File
@@ -177,6 +177,7 @@ export function LabelBase({
text,
a.font_semi_bold,
a.leading_tight,
a.flex_shrink,
t.atoms.text_contrast_medium,
{paddingRight: 3},
]}>
+2 -2
View File
@@ -59,7 +59,7 @@ export function ImageEmbed({
// Captured from AutoSizedImage so the peek-commit handler can reuse the same
// ref + dims that a tap would — keeps the lightbox's return animation intact.
const singleContainerRef = useRef<AnimatedRef<React.Component> | null>(null)
const singleContainerRef = useRef<AnimatedRef | null>(null)
const singleDimsRef = useRef<Dimensions | null>(null)
if (images.length > 0) {
@@ -71,7 +71,7 @@ export function ImageEmbed({
}))
const onPress = (
index: number,
refs: AnimatedRef<React.Component>[],
refs: AnimatedRef[],
fetchedDims: (Dimensions | null)[],
) => {
if (postContext) {
+3 -7
View File
@@ -119,7 +119,8 @@ export const ProgressGuideToast = forwardRef<
left = right = (winDim.width - 380) / 2
}
return {
position: IS_WEB ? 'fixed' : 'absolute',
// position: fixed is web only
position: (IS_WEB ? 'fixed' : 'absolute') as 'absolute',
top: 0,
left,
right,
@@ -134,12 +135,7 @@ export const ProgressGuideToast = forwardRef<
return (
isOpen && (
<Portal>
<Animated.View
style={[
// @ts-ignore position: fixed is web only
containerStyle,
animatedStyle,
]}>
<Animated.View style={[containerStyle, animatedStyle]}>
<Pressable
style={[
t.atoms.bg,
+2 -1
View File
@@ -8,6 +8,7 @@ import {
type ViewStyle,
} from 'react-native'
import Animated, {
type AnimatedStyle,
FadeIn,
FadeOut,
interpolateColor,
@@ -662,7 +663,7 @@ function BlockedPlaceholder({
style,
}: {
profile: Shadow<ChatBskyActorDefs.ProfileViewBasic>
style?: StyleProp<ViewStyle>
style?: AnimatedStyle<ViewStyle>
}) {
const {t: l} = useLingui()
const t = useTheme()
-5
View File
@@ -12,7 +12,6 @@ import {
import {HITSLOP_20} from '#/lib/constants'
import {mergeRefs} from '#/lib/merge-refs'
import {
android,
applyFonts,
atoms as a,
platform,
@@ -223,10 +222,6 @@ export function createInput(Component: typeof TextInput) {
paddingTop: 13,
paddingBottom: 13,
},
android({
paddingTop: 8,
paddingBottom: 9,
}),
/*
* Margins are needed here to avoid autofill background overlapping the
* top and bottom borders - esb