React Native New Arch (#10980)
This commit is contained in:
+20
-41
@@ -72,21 +72,26 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
const isDarkMode = colorScheme === 'dark'
|
||||
|
||||
const logoAnimation = useAnimatedStyle(() => {
|
||||
const introScale = interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp')
|
||||
const outroScale =
|
||||
reduceMotion === true
|
||||
? 1
|
||||
: interpolate(outroLogo.get(), [0, 0.08, 1], [1, 0.8, 500], 'clamp')
|
||||
|
||||
const introOpacity = interpolate(intro.get(), [0, 1], [0, 1], 'clamp')
|
||||
const outroOpacity = interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[1, 1, 0, 0],
|
||||
'clamp',
|
||||
)
|
||||
|
||||
return {
|
||||
opacity: introOpacity * outroOpacity,
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
outroLogo.get(),
|
||||
[0, 0.08, 1],
|
||||
[1, 0.8, 500],
|
||||
'clamp',
|
||||
),
|
||||
},
|
||||
{translateY: -(insets.top / 2)},
|
||||
{scale: 0.1 * outroScale * introScale},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const bottomLogoAnimation = useAnimatedStyle(() => {
|
||||
@@ -94,27 +99,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
const reducedLogoAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
||||
},
|
||||
],
|
||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
||||
}
|
||||
})
|
||||
|
||||
const logoWrapperAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[1, 1, 0, 0],
|
||||
'clamp',
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
const appAnimation = useAnimatedStyle(() => {
|
||||
return {
|
||||
@@ -126,7 +110,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
opacity: interpolate(
|
||||
outroAppOpacity.get(),
|
||||
[0, 0.1, 0.2, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0.02, 0.02, 1, 1], // first two values cant be 0 for the iOS blur/glass effects to work, the values obtained by trial and error
|
||||
'clamp',
|
||||
),
|
||||
}
|
||||
@@ -180,8 +164,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
||||
}, [])
|
||||
|
||||
const logoAnimations =
|
||||
reduceMotion === true ? reducedLogoAnimation : logoAnimation
|
||||
// special off-spec color for dark mode
|
||||
const logoBg = isDarkMode ? '#0F1824' : '#fff'
|
||||
|
||||
@@ -224,17 +206,14 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
||||
<Animated.View
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
logoWrapperAnimation,
|
||||
logoAnimation,
|
||||
{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
||||
},
|
||||
]}>
|
||||
<Animated.View style={[logoAnimations]}>
|
||||
<Logo fill={logoBg} />
|
||||
</Animated.View>
|
||||
<Logo fill={logoBg} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MMKV} from '@bsky.app/react-native-mmkv'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
import {setPolyfills} from '@growthbook/growthbook'
|
||||
import {GrowthBook} from '@growthbook/growthbook-react'
|
||||
import {type I18n} from '@lingui/core'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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},
|
||||
]}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {beforeEach, describe, expect, it, jest} from '@jest/globals'
|
||||
|
||||
jest.mock('@bsky.app/react-native-mmkv', () => ({
|
||||
jest.mock('react-native-mmkv', () => ({
|
||||
MMKV: class MMKVMock {
|
||||
_store = new Map<string, string>()
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ export function AnimatedLikeIcon({
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: size / 2,
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
<Animated.View
|
||||
@@ -121,6 +122,7 @@ export function AnimatedLikeIcon({
|
||||
zIndex: -1,
|
||||
pointerEvents: 'none',
|
||||
borderRadius: size / 2,
|
||||
opacity: 0,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
KeyboardGestureArea,
|
||||
} from 'react-native-keyboard-controller'
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
runOnJS,
|
||||
type ScrollEvent,
|
||||
type SharedValue,
|
||||
@@ -187,6 +186,14 @@ export function MessagesList({
|
||||
}
|
||||
}, [hasScrolled, listOpacity])
|
||||
|
||||
// Recreate FadeIn for the footer with a shared value so we can start from a
|
||||
// non-zero opacity instead of fully transparent. (Needed for GlassView to work properly)
|
||||
const footerOpacity = useSharedValue(0.05)
|
||||
|
||||
useEffect(() => {
|
||||
footerOpacity.set(withTiming(1, {duration: 200}))
|
||||
}, [footerOpacity])
|
||||
|
||||
const inputHeightUI = useSharedValue(0)
|
||||
const [inputHeightJS, setInputHeightJS] = useState(0)
|
||||
|
||||
@@ -753,6 +760,10 @@ export function MessagesList({
|
||||
opacity: listOpacity.get(),
|
||||
}))
|
||||
|
||||
const animatedFooterStyle = useAnimatedStyle(() => ({
|
||||
opacity: footerOpacity.get(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<InviteLinkDialogProvider convo={convoState.convo}>
|
||||
<MessageRepliesProvider scrollToMessage={scrollToMessage}>
|
||||
@@ -841,7 +852,7 @@ export function MessagesList({
|
||||
opened: 0,
|
||||
}}>
|
||||
{footer ?? (
|
||||
<Animated.View entering={FadeIn.duration(200)}>
|
||||
<Animated.View style={animatedFooterStyle}>
|
||||
<ConversationFooter
|
||||
convoState={convoState}
|
||||
hasAcceptOverride={hasAcceptOverride}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
RQKEY as FEED_RQKEY,
|
||||
} from '#/state/queries/post-feed'
|
||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||
import {PostFeed, type PostFeedRef} from '#/view/com/posts/PostFeed'
|
||||
import {
|
||||
EmptyState,
|
||||
type EmptyStateButtonProps,
|
||||
@@ -36,6 +36,7 @@ interface FeedSectionProps {
|
||||
emptyStateMessage?: string
|
||||
emptyStateButton?: EmptyStateButtonProps
|
||||
emptyStateIcon?: React.ComponentType<any> | React.ReactElement
|
||||
postFeedRef?: React.Ref<PostFeedRef>
|
||||
}
|
||||
|
||||
export function ProfileFeedSection({
|
||||
@@ -49,6 +50,7 @@ export function ProfileFeedSection({
|
||||
emptyStateMessage,
|
||||
emptyStateButton,
|
||||
emptyStateIcon,
|
||||
postFeedRef,
|
||||
}: FeedSectionProps) {
|
||||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -111,6 +113,7 @@ export function ProfileFeedSection({
|
||||
shouldUseAdjustedNumToRender ? adjustedInitialNumToRender : undefined
|
||||
}
|
||||
isVideoFeed={isVideoFeed}
|
||||
ref={postFeedRef}
|
||||
/>
|
||||
{(isScrolledDown || hasNew) && (
|
||||
<LoadLatestBtn
|
||||
|
||||
@@ -8,7 +8,6 @@ import {type InfiniteData} from '@tanstack/react-query'
|
||||
import {popularInterests, useInterestsDisplayNames} from '#/lib/interests'
|
||||
import {logger} from '#/logger'
|
||||
import {usePreferencesQuery} from '#/state/queries/preferences'
|
||||
import {BlockDrawerGesture} from '#/view/shell/BlockDrawerGesture'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
@@ -71,26 +70,24 @@ export function SuggestedAccountsTabBar({
|
||||
.sort(boostInterests(personalizedInterests))
|
||||
|
||||
return (
|
||||
<BlockDrawerGesture>
|
||||
<InterestTabs
|
||||
interests={hideDefaultTab ? interests : ['all', ...interests]}
|
||||
selectedInterest={
|
||||
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
|
||||
}
|
||||
onSelectTab={tab => {
|
||||
ax.metric('explore:suggestedAccounts:tabPressed', {tab: tab})
|
||||
onSelectInterest(tab === 'all' ? null : tab)
|
||||
}}
|
||||
interestsDisplayNames={
|
||||
hideDefaultTab
|
||||
? interestsDisplayNames
|
||||
: {
|
||||
all: defaultTabLabel || _(msg`For You`),
|
||||
...interestsDisplayNames,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</BlockDrawerGesture>
|
||||
<InterestTabs
|
||||
interests={hideDefaultTab ? interests : ['all', ...interests]}
|
||||
selectedInterest={
|
||||
selectedInterest || (hideDefaultTab ? interests[0] : 'all')
|
||||
}
|
||||
onSelectTab={tab => {
|
||||
ax.metric('explore:suggestedAccounts:tabPressed', {tab: tab})
|
||||
onSelectInterest(tab === 'all' ? null : tab)
|
||||
}}
|
||||
interestsDisplayNames={
|
||||
hideDefaultTab
|
||||
? interestsDisplayNames
|
||||
: {
|
||||
all: defaultTabLabel || _(msg`For You`),
|
||||
...interestsDisplayNames,
|
||||
}
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {useSetThemePrefs, useThemePrefs} from '#/state/shell'
|
||||
import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem'
|
||||
import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf'
|
||||
import * as SegmentedControl from '#/components/forms/SegmentedControl'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
@@ -24,7 +23,6 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T
|
||||
import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_INTERNAL, IS_NATIVE} from '#/env'
|
||||
import * as SettingsList from './components/SettingsList'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
|
||||
@@ -165,12 +163,12 @@ export function AppearanceSettingsScreen({}: Props) {
|
||||
onChange={onChangeFontScale}
|
||||
/>
|
||||
|
||||
{IS_NATIVE && IS_INTERNAL && (
|
||||
{/*{IS_NATIVE && IS_INTERNAL && (
|
||||
<>
|
||||
<SettingsList.Divider />
|
||||
<AppIconSettingsListItem />
|
||||
</>
|
||||
)}
|
||||
)}*/}
|
||||
</Animated.View>
|
||||
</SettingsList.Container>
|
||||
</Layout.Content>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import {
|
||||
Reanimated3DefaultSpringConfig,
|
||||
type SharedValue,
|
||||
useSharedValue,
|
||||
withSpring,
|
||||
@@ -34,6 +35,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
'worklet'
|
||||
footerMode.set(() =>
|
||||
withSpring(v ? 1 : 0, {
|
||||
...Reanimated3DefaultSpringConfig,
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import {beforeEach, expect, jest, test} from '@jest/globals'
|
||||
|
||||
import {Storage} from '#/storage'
|
||||
|
||||
jest.mock('@bsky.app/react-native-mmkv', () => ({
|
||||
jest.mock('react-native-mmkv', () => ({
|
||||
MMKV: class MMKVMock {
|
||||
_store = new Map()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {MMKV} from '@bsky.app/react-native-mmkv'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
|
||||
import {type DB} from '#/storage/archive/db/types'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {MMKV} from '@bsky.app/react-native-mmkv'
|
||||
import {MMKV} from 'react-native-mmkv'
|
||||
|
||||
import {type Account, type Device} from '#/storage/schema'
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ export const SplashScreen = ({
|
||||
logoFill,
|
||||
logoShadow: isDarkMode
|
||||
? [
|
||||
t.atoms.shadow_md,
|
||||
{
|
||||
shadowColor: logoFill,
|
||||
shadowRadius: 8,
|
||||
shadowOpacity: 0.5,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
@@ -93,13 +93,15 @@ export const SplashScreen = ({
|
||||
size="large"
|
||||
color={isDarkMode ? 'secondary_inverted' : 'secondary'}
|
||||
style={[
|
||||
t.atoms.shadow_md,
|
||||
{
|
||||
shadowColor: t.palette.black,
|
||||
shadowRadius: 8,
|
||||
shadowOpacity: 0.1,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 5,
|
||||
},
|
||||
elevation: 16,
|
||||
},
|
||||
]}>
|
||||
<ButtonText>
|
||||
|
||||
@@ -25,6 +25,7 @@ import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
|
||||
import ProgressCircle from 'react-native-progress/Circle'
|
||||
import Animated, {
|
||||
type AnimatedRef,
|
||||
type AnimatedStyle,
|
||||
Easing,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
@@ -1794,7 +1795,7 @@ function ComposerTopBar({
|
||||
isEditingDraft: boolean
|
||||
canSaveDraft: boolean
|
||||
textLength: number
|
||||
topBarAnimatedStyle: StyleProp<ViewStyle>
|
||||
topBarAnimatedStyle: AnimatedStyle<ViewStyle>
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
const t = useTheme()
|
||||
@@ -2029,7 +2030,7 @@ function ComposerPills({
|
||||
thread: ThreadDraft
|
||||
post: PostDraft
|
||||
dispatch: (action: ComposerAction) => void
|
||||
bottomBarAnimatedStyle: StyleProp<ViewStyle>
|
||||
bottomBarAnimatedStyle: AnimatedStyle<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const media = post.embed.media
|
||||
|
||||
@@ -227,7 +227,6 @@ export function TextInput({
|
||||
allowFontScaling
|
||||
multiline
|
||||
scrollEnabled={false}
|
||||
numberOfLines={2}
|
||||
// Note: should be the default value, but as of v1.104
|
||||
// it switched to "none" on Android
|
||||
autoCapitalize="sentences"
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AppState,
|
||||
@@ -195,6 +203,10 @@ export function getItemsForFeedback(feedRow: FeedRow): {
|
||||
}
|
||||
}
|
||||
|
||||
export type PostFeedRef = {
|
||||
refreshFeed: () => Promise<void>
|
||||
}
|
||||
|
||||
// DISABLED need to check if this is causing random feed refreshes -prf
|
||||
// const REFRESH_AFTER = STALE.HOURS.ONE
|
||||
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
|
||||
@@ -222,6 +234,7 @@ let PostFeed = ({
|
||||
savedFeedConfig,
|
||||
initialNumToRender: initialNumToRenderOverride,
|
||||
isVideoFeed = false,
|
||||
ref,
|
||||
}: {
|
||||
feed: FeedDescriptor
|
||||
description?: RichTextType
|
||||
@@ -246,6 +259,7 @@ let PostFeed = ({
|
||||
initialNumToRender?: number
|
||||
isVideoFeed?: boolean
|
||||
lastFetchDate?: () => number
|
||||
ref?: React.Ref<PostFeedRef>
|
||||
}): React.ReactNode => {
|
||||
const ax = useAnalytics()
|
||||
const t = useTheme()
|
||||
@@ -725,8 +739,9 @@ let PostFeed = ({
|
||||
|
||||
// events
|
||||
// =
|
||||
//
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
const refreshFeed = async () => {
|
||||
if (!enabled) return
|
||||
|
||||
ax.metric('feed:refresh', {
|
||||
@@ -734,24 +749,23 @@ let PostFeed = ({
|
||||
feedUrl: feed,
|
||||
reason: 'pull-to-refresh',
|
||||
})
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await truncateAndInvalidate(queryClient, RQKEY(feed, feedParams))
|
||||
onHasNew?.(false)
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh posts feed', {message: err})
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
setIsPTRing(true)
|
||||
await refreshFeed()
|
||||
setIsPTRing(false)
|
||||
}, [
|
||||
ax,
|
||||
queryClient,
|
||||
setIsPTRing,
|
||||
onHasNew,
|
||||
feed,
|
||||
feedParams,
|
||||
feedType,
|
||||
enabled,
|
||||
])
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
refreshFeed,
|
||||
}))
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
|
||||
@@ -3,6 +3,7 @@ import {type NativeScrollEvent} from 'react-native'
|
||||
import {
|
||||
clamp,
|
||||
interpolate,
|
||||
Reanimated3DefaultSpringConfig,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
@@ -105,6 +106,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) {
|
||||
'worklet'
|
||||
headerMode.set(() =>
|
||||
withSpring(v ? 1 : 0, {
|
||||
...Reanimated3DefaultSpringConfig,
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react'
|
||||
import {ActivityIndicator, StyleSheet} from 'react-native'
|
||||
import {withSpring} from 'react-native-reanimated'
|
||||
import {
|
||||
Reanimated3DefaultSpringConfig,
|
||||
withSpring,
|
||||
} from 'react-native-reanimated'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||
@@ -147,7 +150,12 @@ function HomeScreenReady({
|
||||
const headerMode = useHomeHeaderMode()
|
||||
const showHeader = useCallback(() => {
|
||||
'worklet'
|
||||
headerMode.set(() => withSpring(0, {overshootClamping: true}))
|
||||
headerMode.set(() =>
|
||||
withSpring(0, {
|
||||
...Reanimated3DefaultSpringConfig,
|
||||
overshootClamping: true,
|
||||
}),
|
||||
)
|
||||
}, [headerMode])
|
||||
|
||||
useFocusEffect(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||
import {StyleSheet} from 'react-native'
|
||||
import {SafeAreaView} from 'react-native-safe-area-context'
|
||||
import {ScrollForwarderView} from 'react-native-scroll-forwarder/src'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
moderateProfile,
|
||||
@@ -36,6 +37,7 @@ import {useAgent, useSession} from '#/state/session'
|
||||
import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens'
|
||||
import {ProfileLists} from '#/view/com/lists/ProfileLists'
|
||||
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
||||
import {type PostFeedRef} from '#/view/com/posts/PostFeed'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {FAB} from '#/view/com/util/fab/FAB'
|
||||
import {type ListRef} from '#/view/com/util/List'
|
||||
@@ -53,7 +55,6 @@ import * as Layout from '#/components/Layout'
|
||||
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
||||
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
||||
import {navigate} from '#/Navigation'
|
||||
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
|
||||
|
||||
interface SectionRef {
|
||||
scrollToTop: () => void
|
||||
@@ -187,6 +188,7 @@ function ProfileScreenLoaded({
|
||||
enabled: !!profile.associated?.labeler,
|
||||
})
|
||||
const [currentPage, setCurrentPage] = useState(0)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const {_} = useLingui()
|
||||
|
||||
const [scrollViewTag, setScrollViewTag] = useState<number | null>(null)
|
||||
@@ -353,6 +355,14 @@ function ProfileScreenLoaded({
|
||||
],
|
||||
})
|
||||
|
||||
const postFeedRef = useRef<PostFeedRef>(null)
|
||||
|
||||
const onRefresh = async () => {
|
||||
setIsRefreshing(true)
|
||||
await postFeedRef.current?.refreshFeed()
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
@@ -362,7 +372,10 @@ function ProfileScreenLoaded({
|
||||
setMinimumHeight: (height: number) => void
|
||||
}) => {
|
||||
return (
|
||||
<ExpoScrollForwarderView scrollViewTag={scrollViewTag}>
|
||||
<ScrollForwarderView
|
||||
scrollViewTag={scrollViewTag}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}>
|
||||
<ProfileHeader
|
||||
profile={profile}
|
||||
labeler={labelerInfo}
|
||||
@@ -372,7 +385,7 @@ function ProfileScreenLoaded({
|
||||
isPlaceholderProfile={showPlaceholder}
|
||||
setMinimumHeight={setMinimumHeight}
|
||||
/>
|
||||
</ExpoScrollForwarderView>
|
||||
</ScrollForwarderView>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -440,6 +453,7 @@ function ProfileScreenLoaded({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
postFeedRef={postFeedRef}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
||||
@@ -2,6 +2,11 @@ import {useContext} from 'react'
|
||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||
|
||||
/**
|
||||
* BlockDrawerGesture must wrap the ScrollView directly - Gesture.Native()
|
||||
* only works when attached to the natively scrollable view. On the new
|
||||
* arch (Android), attaching it to a wrapper view breaks scrolling.
|
||||
*/
|
||||
export function BlockDrawerGesture({children}: {children: React.ReactNode}) {
|
||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||
let scrollGesture = Gesture.Native()
|
||||
|
||||
Reference in New Issue
Block a user