This commit is contained in:
Dan Abramov
2023-11-10 13:44:42 +00:00
parent cb14cbb153
commit a3348aca15
2 changed files with 61 additions and 363 deletions
+29 -114
View File
@@ -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
+32 -249
View File
@@ -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]
} }