Files
frontend/app/_layout.tsx
lafay c903990aaf
Some checks failed
Frontend CI / ota-android (push) Has been cancelled
Frontend CI / build-and-push-web (push) Has been cancelled
Frontend CI / build-android-apk (push) Has been cancelled
feat(APKUpdate): implement APK update check and enhance build workflow
- Added `expo-intent-launcher` dependency to support APK launching functionality.
- Introduced `APKUpdateBootstrap` component to check for APK updates during app initialization.
- Enhanced build workflow in `build.yml` to resolve runtime version and upload APK to the updates server.
- Updated `HomeScreen` and `TabsLayout` to manage tab visibility and scroll behavior based on user interactions.
- Refactored message bubble styles for improved UI consistency and user experience.
2026-03-26 03:41:43 +08:00

228 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import { checkForAPKUpdate } from '../src/services/apkUpdateService';
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 APKUpdateBootstrap() {
const hasChecked = useRef(false);
useEffect(() => {
if (hasChecked.current) return;
hasChecked.current = true;
// 延迟执行,避免与启动流程冲突
const timer = setTimeout(() => {
checkForAPKUpdate().catch((error) => {
console.error('APK update check failed:', error);
});
}, 3000);
return () => clearTimeout(timer);
}, []);
return null;
}
function ThemedStack() {
const router = useRouter();
const colors = useAppColors();
const resolved = useResolvedColorScheme();
return (
<>
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
<SystemChrome />
<SessionGate>
<NotificationBootstrap />
<APKUpdateBootstrap />
<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>
);
}