236 lines
8.8 KiB
TypeScript
236 lines
8.8 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {PortalHost, PortalProvider} from '@gorhom/portal';
|
|
import {Provider as EMMProvider} from '@mattermost/react-native-emm';
|
|
import RNUtils from '@mattermost/rnutils';
|
|
import {Stack, useNavigationContainerRef} from 'expo-router';
|
|
import * as SplashScreen from 'expo-splash-screen';
|
|
import {useCallback, useEffect, useMemo, useState} from 'react';
|
|
import {IntlProvider} from 'react-intl';
|
|
import {Keyboard, Platform, StyleSheet} from 'react-native';
|
|
import {GestureHandlerRootView} from 'react-native-gesture-handler';
|
|
import {KeyboardProvider} from 'react-native-keyboard-controller';
|
|
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
|
import tinycolor from 'tinycolor2';
|
|
|
|
import {Screens} from '@constants';
|
|
import {isEdgeToEdge} from '@constants/device';
|
|
import {useThemeByAppearanceWithDefault} from '@context/theme';
|
|
import useDidMount from '@hooks/did_mount';
|
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
|
import {cleanup, initialize} from '@init/app';
|
|
import InAppNotificationContainer from '@screens/in_app_notification';
|
|
import ReviewAppContainer from '@screens/review_app';
|
|
import ShareFeedbackContainer from '@screens/share_feedback';
|
|
import SnackBarContainer from '@screens/snack_bar';
|
|
import WatermarkContainer from '@screens/watermark';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import {NavigationStore, useCurrentScreen} from '@store/navigation_store';
|
|
|
|
import type {NativeStackNavigationOptions} from '@react-navigation/native-stack';
|
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
|
|
|
const loginFlowScreens = new Set<AvailableScreens>([
|
|
Screens.ONBOARDING,
|
|
Screens.SERVER,
|
|
Screens.LOGIN,
|
|
Screens.SSO,
|
|
Screens.MFA,
|
|
Screens.FORGOT_PASSWORD,
|
|
]);
|
|
|
|
const styles = StyleSheet.create({
|
|
gestureHandlerRootView: {
|
|
flex: 1,
|
|
},
|
|
});
|
|
|
|
// Prevent splash screen from auto-hiding
|
|
SplashScreen.setOptions({fade: true});
|
|
SplashScreen.preventAutoHideAsync();
|
|
|
|
export default function RootLayout() {
|
|
const [appReady, setAppReady] = useState(false);
|
|
const navigationRef = useNavigationContainerRef();
|
|
const currentScreen = useCurrentScreen();
|
|
const [theme, setTheme] = useState(EphemeralStore.getTheme());
|
|
const appearanceTheme = useThemeByAppearanceWithDefault();
|
|
|
|
const setAndroidNavigationBarColor = useCallback(() => {
|
|
if (Platform.OS === 'android') {
|
|
let t = theme;
|
|
if (currentScreen && loginFlowScreens.has(currentScreen)) {
|
|
t = appearanceTheme;
|
|
}
|
|
RNUtils.setNavigationBarColor(t.centerChannelBg, tinycolor(t.centerChannelBg).isLight());
|
|
}
|
|
}, [theme, appearanceTheme, currentScreen]);
|
|
|
|
useDidMount(() => {
|
|
const subscription = EphemeralStore.observeTheme().subscribe((newTheme) => {
|
|
setTheme(newTheme);
|
|
});
|
|
return () => {
|
|
subscription.unsubscribe();
|
|
};
|
|
});
|
|
|
|
useDidMount(() => {
|
|
async function initializeApp() {
|
|
try {
|
|
await initialize();
|
|
RNUtils.lockPortrait();
|
|
|
|
setAppReady(true);
|
|
} catch (error) {
|
|
setAppReady(true); // Still show UI with error state
|
|
}
|
|
}
|
|
|
|
initializeApp();
|
|
|
|
return () => {
|
|
// Cleanup on unmount
|
|
cleanup();
|
|
};
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (appReady) {
|
|
SplashScreen.hideAsync();
|
|
}
|
|
}, [appReady]);
|
|
|
|
useEffect(() => {
|
|
const handleKeyboardHide = () => {
|
|
// Add small delay to ensure keyboard animation completes before resetting color
|
|
// Android keyboard can override the color change if called too early
|
|
setTimeout(setAndroidNavigationBarColor, 100);
|
|
};
|
|
|
|
const keyboardListener = Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
|
|
|
|
return () => {
|
|
keyboardListener.remove();
|
|
};
|
|
}, [setAndroidNavigationBarColor]);
|
|
|
|
useEffect(() => {
|
|
setAndroidNavigationBarColor();
|
|
}, [setAndroidNavigationBarColor]);
|
|
|
|
// Track navigation state changes
|
|
useEffect(() => {
|
|
const handleStateChange = () => {
|
|
const state = navigationRef.getRootState();
|
|
NavigationStore.updateFromNavigationState(state);
|
|
};
|
|
|
|
// Wait for navigation to be ready, then set up listeners
|
|
const setupNavigation = () => {
|
|
if (navigationRef.isReady()) {
|
|
// Subscribe to navigation state changes
|
|
const unsubscribe = navigationRef.addListener('state', handleStateChange);
|
|
|
|
// Get initial state
|
|
handleStateChange();
|
|
|
|
return unsubscribe;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// Try to set up immediately if ready
|
|
let unsubscribe = setupNavigation();
|
|
|
|
// If not ready, wait for the 'state' event which will fire once ready
|
|
if (!unsubscribe) {
|
|
const readyUnsubscribe = navigationRef.addListener('state', () => {
|
|
if (!unsubscribe && navigationRef.isReady()) {
|
|
unsubscribe = setupNavigation();
|
|
readyUnsubscribe();
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
readyUnsubscribe();
|
|
unsubscribe?.();
|
|
};
|
|
}
|
|
|
|
return unsubscribe;
|
|
}, [navigationRef]);
|
|
|
|
const stackScreenOptions = useMemo<NativeStackNavigationOptions>(() => ({
|
|
headerShown: false,
|
|
animation: 'none',
|
|
contentStyle: {backgroundColor: theme.centerChannelBg},
|
|
...Platform.select({android: {statusBarBackgroundColor: theme.sidebarBg, statusBarTranslucent: isEdgeToEdge, statusBarStyle: tinycolor(theme.sidebarBg).isLight() ? 'dark' : 'light'}}),
|
|
}), [theme.centerChannelBg, theme.sidebarBg]);
|
|
|
|
const modalScreenOptions = useMemo<NativeStackNavigationOptions>(() => ({
|
|
presentation: 'modal',
|
|
animation: 'slide_from_bottom',
|
|
headerShown: false,
|
|
}), []);
|
|
|
|
const bottomSheetScreenOptions = useMemo<NativeStackNavigationOptions>(() => ({
|
|
presentation: 'transparentModal',
|
|
animation: 'none',
|
|
headerShown: false,
|
|
contentStyle: {backgroundColor: 'transparent'},
|
|
...Platform.select({android: {statusBarBackgroundColor: theme.sidebarBg, statusBarTranslucent: isEdgeToEdge}}),
|
|
}), [theme.sidebarBg]);
|
|
|
|
if (!appReady) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<GestureHandlerRootView style={styles.gestureHandlerRootView}>
|
|
<SafeAreaProvider>
|
|
<EMMProvider>
|
|
<IntlProvider
|
|
locale={DEFAULT_LOCALE}
|
|
messages={getTranslations(DEFAULT_LOCALE)}
|
|
>
|
|
<PortalProvider>
|
|
<KeyboardProvider
|
|
enabled={isEdgeToEdge}
|
|
statusBarTranslucent={isEdgeToEdge}
|
|
navigationBarTranslucent={isEdgeToEdge}
|
|
preserveEdgeToEdge={isEdgeToEdge}
|
|
preload={true}
|
|
>
|
|
<Stack screenOptions={stackScreenOptions}>
|
|
<Stack.Screen name='(unauthenticated)'/>
|
|
<Stack.Screen name='(authenticated)'/>
|
|
<Stack.Screen name='index'/>
|
|
<Stack.Screen
|
|
name='(modals)'
|
|
options={modalScreenOptions}
|
|
/>
|
|
<Stack.Screen
|
|
name='(bottom_sheet)'
|
|
options={bottomSheetScreenOptions}
|
|
/>
|
|
</Stack>
|
|
<PortalHost name='snack_bar'/>
|
|
<SnackBarContainer/>
|
|
<PortalHost name='watermark'/>
|
|
<WatermarkContainer/>
|
|
</KeyboardProvider>
|
|
<PortalHost name='notification'/>
|
|
<InAppNotificationContainer/>
|
|
<PortalHost name='review_app'/>
|
|
<ReviewAppContainer/>
|
|
<PortalHost name='share_feedback'/>
|
|
<ShareFeedbackContainer/>
|
|
</PortalProvider>
|
|
</IntlProvider>
|
|
</EMMProvider>
|
|
</SafeAreaProvider>
|
|
</GestureHandlerRootView>
|
|
);
|
|
}
|