repro
This commit is contained in:
+29
-114
@@ -1,122 +1,37 @@
|
|||||||
import 'react-native-url-polyfill/auto'
|
import * as React from 'react'
|
||||||
import 'lib/sentry' // must be near top
|
import {View} from 'react-native'
|
||||||
|
import Animated, {useAnimatedScrollHandler} from 'react-native-reanimated'
|
||||||
import React, {useState, useEffect} from 'react'
|
|
||||||
import {RootSiblingParent} from 'react-native-root-siblings'
|
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
|
||||||
import {GestureHandlerRootView} from 'react-native-gesture-handler'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import {QueryClientProvider} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import 'view/icons'
|
|
||||||
|
|
||||||
import {init as initPersistedState} from '#/state/persisted'
|
|
||||||
import {useColorMode} from 'state/shell'
|
|
||||||
import {ThemeProvider} from 'lib/ThemeContext'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
|
||||||
import {Shell} from 'view/shell'
|
|
||||||
import * as notifications from 'lib/notifications/notifications'
|
|
||||||
import * as analytics from 'lib/analytics/analytics'
|
|
||||||
import * as Toast from 'view/com/util/Toast'
|
|
||||||
import {queryClient} from 'lib/react-query'
|
|
||||||
import {TestCtrls} from 'view/com/testing/TestCtrls'
|
|
||||||
import {Provider as ShellStateProvider} from 'state/shell'
|
|
||||||
import {Provider as ModalStateProvider} from 'state/modals'
|
|
||||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
|
||||||
import {Provider as InvitesStateProvider} from 'state/invites'
|
|
||||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
|
||||||
import {
|
|
||||||
Provider as SessionProvider,
|
|
||||||
useSession,
|
|
||||||
useSessionApi,
|
|
||||||
} from 'state/session'
|
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
import {i18n} from '@lingui/core'
|
|
||||||
import {I18nProvider} from '@lingui/react'
|
|
||||||
import {messages} from './locale/locales/en/messages'
|
|
||||||
i18n.load('en', messages)
|
|
||||||
i18n.activate('en')
|
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync()
|
|
||||||
|
|
||||||
const InnerApp = observer(function AppImpl() {
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
const {isInitialLoad} = useSession()
|
|
||||||
const {resumeSession} = useSessionApi()
|
|
||||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
// init
|
|
||||||
useEffect(() => {
|
|
||||||
setupState().then(store => {
|
|
||||||
setRootStore(store)
|
|
||||||
analytics.init(store)
|
|
||||||
notifications.init(store)
|
|
||||||
store.onSessionDropped(() => {
|
|
||||||
Toast.show('Sorry! Your session expired. Please log in again.')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const account = persisted.get('session').currentAccount
|
|
||||||
resumeSession(account)
|
|
||||||
}, [resumeSession])
|
|
||||||
|
|
||||||
// show nothing prior to init
|
|
||||||
if (!rootStore || isInitialLoad) {
|
|
||||||
// TODO add a loading state
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ThemeProvider theme={colorMode}>
|
|
||||||
<RootSiblingParent>
|
|
||||||
<analytics.Provider>
|
|
||||||
<RootStoreProvider value={rootStore}>
|
|
||||||
<I18nProvider i18n={i18n}>
|
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
|
||||||
<TestCtrls />
|
|
||||||
<Shell />
|
|
||||||
</GestureHandlerRootView>
|
|
||||||
</I18nProvider>
|
|
||||||
</RootStoreProvider>
|
|
||||||
</analytics.Provider>
|
|
||||||
</RootSiblingParent>
|
|
||||||
</ThemeProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [isReady, setReady] = useState(false)
|
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [x, setX] = React.useState(0)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
initPersistedState().then(() => setReady(true))
|
let id = setInterval(() => setX(x => x + 1), 1000)
|
||||||
|
return () => clearInterval(id)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!isReady) {
|
// props to pass into children render functions
|
||||||
return null
|
const onScroll = useAnimatedScrollHandler({
|
||||||
}
|
onScroll(e) {
|
||||||
|
console.log(x)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider>
|
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||||
<ShellStateProvider>
|
<Animated.FlatList
|
||||||
<PrefsStateProvider>
|
onScroll={onScroll}
|
||||||
<MutedThreadsProvider>
|
style={{flex: 1, backgroundColor: 'red'}}
|
||||||
<InvitesStateProvider>
|
contentContainerStyle={{
|
||||||
<ModalStateProvider>
|
paddingTop: 1000,
|
||||||
<InnerApp />
|
}}
|
||||||
</ModalStateProvider>
|
/>
|
||||||
</InvitesStateProvider>
|
<Animated.FlatList
|
||||||
</MutedThreadsProvider>
|
onScroll={onScroll}
|
||||||
</PrefsStateProvider>
|
style={{flex: 1, backgroundColor: 'blue'}}
|
||||||
</ShellStateProvider>
|
contentContainerStyle={{
|
||||||
</SessionProvider>
|
paddingTop: 1000,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
|
||||||
|
|||||||
@@ -1,254 +1,37 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import {
|
import {View} from 'react-native'
|
||||||
FlatList,
|
import Animated, {useAnimatedScrollHandler} from 'react-native-reanimated'
|
||||||
LayoutChangeEvent,
|
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import Animated, {
|
|
||||||
useAnimatedReaction,
|
|
||||||
useAnimatedScrollHandler,
|
|
||||||
useAnimatedStyle,
|
|
||||||
useSharedValue,
|
|
||||||
runOnJS,
|
|
||||||
scrollTo,
|
|
||||||
useAnimatedRef,
|
|
||||||
SharedValue,
|
|
||||||
} from 'react-native-reanimated'
|
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
|
||||||
import {TabBar} from './TabBar'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
|
||||||
|
|
||||||
interface PagerWithHeaderChildParams {
|
export function PagerWithHeader() {
|
||||||
headerHeight: number
|
const [x, setX] = React.useState(0)
|
||||||
onScroll: OnScrollCb
|
React.useEffect(() => {
|
||||||
isScrolledDown: boolean
|
let id = setInterval(() => setX(x => x + 1), 1000)
|
||||||
scrollElRef: React.MutableRefObject<FlatList<any> | ScrollView | null>
|
return () => clearInterval(id)
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
export interface PagerWithHeaderProps {
|
// props to pass into children render functions
|
||||||
testID?: string
|
const onScroll = useAnimatedScrollHandler({
|
||||||
children:
|
onScroll(e) {
|
||||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
console.log(x)
|
||||||
| ((props: PagerWithHeaderChildParams) => JSX.Element)
|
},
|
||||||
items: string[]
|
|
||||||
isHeaderReady: boolean
|
|
||||||
renderHeader?: () => JSX.Element
|
|
||||||
initialPage?: number
|
|
||||||
onPageSelected?: (index: number) => void
|
|
||||||
onCurrentPageSelected?: (index: number) => void
|
|
||||||
}
|
|
||||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|
||||||
function PageWithHeaderImpl(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
testID,
|
|
||||||
items,
|
|
||||||
isHeaderReady,
|
|
||||||
renderHeader,
|
|
||||||
initialPage,
|
|
||||||
onPageSelected,
|
|
||||||
onCurrentPageSelected,
|
|
||||||
}: PagerWithHeaderProps,
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const [currentPage, setCurrentPage] = React.useState(0)
|
|
||||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
|
||||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
|
||||||
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
|
|
||||||
const scrollY = useSharedValue(0)
|
|
||||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
|
||||||
|
|
||||||
// capture the header bar sizing
|
|
||||||
const onTabBarLayout = React.useCallback(
|
|
||||||
(evt: LayoutChangeEvent) => {
|
|
||||||
setTabBarHeight(evt.nativeEvent.layout.height)
|
|
||||||
},
|
|
||||||
[setTabBarHeight],
|
|
||||||
)
|
|
||||||
const onHeaderOnlyLayout = React.useCallback(
|
|
||||||
(evt: LayoutChangeEvent) => {
|
|
||||||
setHeaderOnlyHeight(evt.nativeEvent.layout.height)
|
|
||||||
},
|
|
||||||
[setHeaderOnlyHeight],
|
|
||||||
)
|
|
||||||
|
|
||||||
// render the the header and tab bar
|
|
||||||
const headerTransform = useAnimatedStyle(
|
|
||||||
() => ({
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
translateY: Math.min(
|
|
||||||
Math.min(scrollY.value, headerOnlyHeight) * -1,
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[scrollY, headerOnlyHeight],
|
|
||||||
)
|
|
||||||
const renderTabBar = React.useCallback(
|
|
||||||
(props: RenderTabBarFnProps) => {
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
isMobile ? styles.tabBarMobile : styles.tabBarDesktop,
|
|
||||||
headerTransform,
|
|
||||||
]}>
|
|
||||||
<View onLayout={onHeaderOnlyLayout}>{renderHeader?.()}</View>
|
|
||||||
<View
|
|
||||||
onLayout={onTabBarLayout}
|
|
||||||
style={{
|
|
||||||
// Render it immediately to measure it early since its size doesn't depend on the content.
|
|
||||||
// However, keep it invisible until the header above stabilizes in order to prevent jumps.
|
|
||||||
opacity: isHeaderReady ? 1 : 0,
|
|
||||||
pointerEvents: isHeaderReady ? 'auto' : 'none',
|
|
||||||
}}>
|
|
||||||
<TabBar
|
|
||||||
items={items}
|
|
||||||
selectedPage={currentPage}
|
|
||||||
onSelect={props.onSelect}
|
|
||||||
onPressSelected={onCurrentPageSelected}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[
|
|
||||||
items,
|
|
||||||
isHeaderReady,
|
|
||||||
renderHeader,
|
|
||||||
headerTransform,
|
|
||||||
currentPage,
|
|
||||||
onCurrentPageSelected,
|
|
||||||
isMobile,
|
|
||||||
onTabBarLayout,
|
|
||||||
onHeaderOnlyLayout,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const scrollRefs = useSharedValue([])
|
|
||||||
const registerRef = (ref, index) => {
|
|
||||||
scrollRefs.modify(refs => {
|
|
||||||
'worklet'
|
|
||||||
refs[index] = ref
|
|
||||||
return refs
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// props to pass into children render functions
|
|
||||||
const onScroll = useAnimatedScrollHandler({
|
|
||||||
onScroll(e) {
|
|
||||||
const nextScrollY = e.contentOffset.y
|
|
||||||
scrollY.value = nextScrollY
|
|
||||||
if (nextScrollY < headerOnlyHeight) {
|
|
||||||
const refs = scrollRefs.value
|
|
||||||
for (let i = 0; i < refs.length; i++) {
|
|
||||||
scrollTo(refs[i], 0, nextScrollY, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const onPageSelectedInner = React.useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
setCurrentPage(index)
|
|
||||||
onPageSelected?.(index)
|
|
||||||
},
|
|
||||||
[onPageSelected, setCurrentPage],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPageSelecting = React.useCallback((index: number) => {
|
|
||||||
setCurrentPage(index)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pager
|
|
||||||
ref={ref}
|
|
||||||
testID={testID}
|
|
||||||
initialPage={initialPage}
|
|
||||||
onPageSelected={onPageSelectedInner}
|
|
||||||
onPageSelecting={onPageSelecting}
|
|
||||||
renderTabBar={renderTabBar}
|
|
||||||
tabBarPosition="top">
|
|
||||||
{toArray(children)
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((child, i) => {
|
|
||||||
return (
|
|
||||||
<View key={i} collapsable={false}>
|
|
||||||
<PagerItem
|
|
||||||
headerHeight={headerHeight}
|
|
||||||
isReady={
|
|
||||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
|
||||||
}
|
|
||||||
isScrolledDown={isScrolledDown}
|
|
||||||
onScroll={i === currentPage ? onScroll : noop}
|
|
||||||
registerRef={ref => registerRef(ref, i)}
|
|
||||||
renderTab={child}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Pager>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function PagerItem({
|
|
||||||
headerHeight,
|
|
||||||
isReady,
|
|
||||||
isScrolledDown,
|
|
||||||
onScroll,
|
|
||||||
renderTab,
|
|
||||||
registerRef,
|
|
||||||
}: {
|
|
||||||
headerHeight: number
|
|
||||||
isScrolledDown: boolean
|
|
||||||
forcedScrollY: SharedValue<number> | null
|
|
||||||
onScroll: OnScrollCb
|
|
||||||
renderTab: ((props: PagerWithHeaderChildParams) => JSX.Element) | null
|
|
||||||
}) {
|
|
||||||
const scrollElRef = useAnimatedRef()
|
|
||||||
registerRef(scrollElRef)
|
|
||||||
if (!isReady || renderTab == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return renderTab({
|
|
||||||
headerHeight,
|
|
||||||
isScrolledDown,
|
|
||||||
onScroll,
|
|
||||||
scrollElRef: scrollElRef as React.MutableRefObject<
|
|
||||||
FlatList<any> | ScrollView | null
|
|
||||||
>,
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
return (
|
||||||
const styles = StyleSheet.create({
|
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||||
tabBarMobile: {
|
<Animated.FlatList
|
||||||
position: 'absolute',
|
onScroll={onScroll}
|
||||||
zIndex: 1,
|
style={{flex: 1, backgroundColor: 'red'}}
|
||||||
top: 0,
|
contentContainerStyle={{
|
||||||
left: 0,
|
paddingTop: 1000,
|
||||||
width: '100%',
|
}}
|
||||||
},
|
/>
|
||||||
tabBarDesktop: {
|
<Animated.FlatList
|
||||||
position: 'absolute',
|
onScroll={onScroll}
|
||||||
zIndex: 1,
|
style={{flex: 1, backgroundColor: 'blue'}}
|
||||||
top: 0,
|
contentContainerStyle={{
|
||||||
// @ts-ignore Web only -prf
|
paddingTop: 1000,
|
||||||
left: 'calc(50% - 299px)',
|
}}
|
||||||
width: 598,
|
/>
|
||||||
},
|
</View>
|
||||||
})
|
)
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
function toArray<T>(v: T | T[]): T[] {
|
|
||||||
if (Array.isArray(v)) {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return [v]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user