Files
frontend/app/_layout.tsx

206 lines
6.6 KiB
TypeScript
Raw Normal View History

import React, { useEffect, useRef, useState } from 'react';
import { AppState, AppStateStatus, Platform, View, ActivityIndicator } from 'react-native';
import { Stack, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PaperProvider } from 'react-native-paper';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as Notifications from 'expo-notifications';
import * as SystemUI from 'expo-system-ui';
import { registerNotificationPresentationHandler } from '../src/services/notificationPreferences';
import {
ThemeBootstrap,
useAppColors,
usePaperThemeFromStore,
useResolvedColorScheme,
} from '../src/theme';
import { AppBackButton } from '../src/components/common';
import AppPromptBar from '../src/components/common/AppPromptBar';
import AppDialogHost from '../src/components/common/AppDialogHost';
import { installAlertOverride } from '../src/services/alertOverride';
import { useAuthStore } from '../src/stores';
registerNotificationPresentationHandler();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 5 * 60 * 1000,
},
},
});
installAlertOverride();
if (Platform.OS === 'web' && typeof document !== 'undefined') {
const style = document.createElement('style');
style.textContent = `
input:focus, textarea:focus, select:focus {
outline: none !important;
outline-width: 0 !important;
box-shadow: none !important;
}
input:focus-visible, textarea:focus-visible {
outline: none !important;
}
*:focus { outline: none !important; }
*:focus-visible { outline: none !important; }
`;
document.head.appendChild(style);
}
function SystemChrome() {
const colors = useAppColors();
useEffect(() => {
void SystemUI.setBackgroundColorAsync(colors.background.default);
if (Platform.OS === 'web' && typeof document !== 'undefined') {
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) {
meta.setAttribute('content', colors.background.default);
}
}
}, [colors.background.default]);
return null;
}
function SessionGate({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false);
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
const colors = useAppColors();
useEffect(() => {
fetchCurrentUser().finally(() => setReady(true));
}, [fetchCurrentUser]);
if (!ready) {
return (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: colors.background.default,
}}
>
<ActivityIndicator size="large" color={colors.primary.main} />
</View>
);
}
return <>{children}</>;
}
function NotificationBootstrap() {
const appState = useRef<AppStateStatus>(AppState.currentState);
const notificationResponseListener = useRef<Notifications.EventSubscription | null>(null);
useEffect(() => {
const initNotifications = async () => {
const { loadNotificationPreferences } = await import('../src/services/notificationPreferences');
await loadNotificationPreferences();
const { systemNotificationService } = await import('../src/services/systemNotificationService');
await systemNotificationService.initialize();
const { initBackgroundService } = await import('../src/services/backgroundService');
await initBackgroundService();
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
systemNotificationService.clearBadge();
}
appState.current = nextAppState;
});
notificationResponseListener.current = Notifications.addNotificationResponseReceivedListener(
(response) => {
void response.notification.request.content.data;
}
);
const notificationReceivedSubscription = Notifications.addNotificationReceivedListener(
(notification) => {
void notification;
}
);
return () => {
subscription.remove();
notificationResponseListener.current?.remove();
notificationReceivedSubscription.remove();
};
};
initNotifications();
}, []);
return null;
}
function ThemedStack() {
const router = useRouter();
const colors = useAppColors();
const resolved = useResolvedColorScheme();
return (
<>
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
<SystemChrome />
<SessionGate>
<NotificationBootstrap />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(app)" options={{ headerShown: false }} />
<Stack.Screen
name="post/[postId]"
options={{
headerShown: true,
title: '',
// 与 PostDetailScreen 的 SafeAreaViewbackground.default同色避免出现 header/内容之间的色差线
headerStyle: { backgroundColor: colors.background.default },
headerTintColor: colors.text.primary,
headerShadowVisible: false,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}}
/>
<Stack.Screen
name="user/[userId]"
options={{
headerShown: true,
title: '用户主页',
headerStyle: { backgroundColor: colors.background.paper },
headerTintColor: colors.text.primary,
headerBackVisible: false,
headerLeft: () => <AppBackButton onPress={() => router.back()} />,
}}
/>
</Stack>
</SessionGate>
</>
);
}
function ThemedProviders({ children }: { children: React.ReactNode }) {
const theme = usePaperThemeFromStore();
return <PaperProvider theme={theme}>{children}</PaperProvider>;
}
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<ThemedProviders>
<QueryClientProvider client={queryClient}>
<ThemeBootstrap />
<ThemedStack />
<AppPromptBar />
<AppDialogHost />
</QueryClientProvider>
</ThemedProviders>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}