feat: full app integration — all screens wired and working

This commit is contained in:
Yossi Elkrief
2026-03-02 13:02:50 +02:00
parent 47861de821
commit df394019cc
6 changed files with 93 additions and 44 deletions

View File

@@ -3,10 +3,46 @@ import { NavigationContainer, DarkTheme } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { apiService } from '@/services/api.service';
import { rssiService } from '@/services/rssi.service';
import { wsService } from '@/services/ws.service';
import { ThemeProvider } from './src/theme/ThemeContext';
import { usePoseStore } from './src/stores/poseStore';
import { useSettingsStore } from './src/stores/settingsStore';
import { RootNavigator } from './src/navigation/RootNavigator';
export default function App() {
const serverUrl = useSettingsStore((state) => state.serverUrl);
const rssiScanEnabled = useSettingsStore((state) => state.rssiScanEnabled);
useEffect(() => {
apiService.setBaseUrl(serverUrl);
const unsubscribe = wsService.subscribe(usePoseStore.getState().handleFrame);
wsService.connect(serverUrl);
return () => {
unsubscribe();
wsService.disconnect();
};
}, [serverUrl]);
useEffect(() => {
if (!rssiScanEnabled) {
rssiService.stopScanning();
return;
}
const unsubscribe = rssiService.subscribe(() => {
// Consumers can subscribe elsewhere for RSSI events.
});
rssiService.startScanning(2000);
return () => {
unsubscribe();
rssiService.stopScanning();
};
}, [rssiScanEnabled]);
useEffect(() => {
(globalThis as { __appStartTime?: number }).__appStartTime = Date.now();
}, []);

View File

@@ -9,3 +9,16 @@ jest.mock('react-native-wifi-reborn', () => ({
jest.mock('react-native-reanimated', () =>
require('react-native-reanimated/mock')
);
jest.mock('react-native-webview', () => {
const React = require('react');
const { View } = require('react-native');
const MockWebView = (props: unknown) => React.createElement(View, props);
return {
__esModule: true,
default: MockWebView,
WebView: MockWebView,
};
});

View File

@@ -0,0 +1,36 @@
import React, { PropsWithChildren } from 'react';
import { render, type RenderOptions } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ThemeProvider } from '@/theme/ThemeContext';
type TestProvidersProps = PropsWithChildren<object>;
const TestProviders = ({ children }: TestProvidersProps) => (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<ThemeProvider>{children}</ThemeProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);
const TestProvidersWithNavigation = ({ children }: TestProvidersProps) => (
<TestProviders>
<NavigationContainer>{children}</NavigationContainer>
</TestProviders>
);
interface RenderWithProvidersOptions extends Omit<RenderOptions, 'wrapper'> {
withNavigation?: boolean;
}
export const renderWithProviders = (
ui: React.ReactElement,
{ withNavigation, ...options }: RenderWithProvidersOptions = {},
) => {
return render(ui, {
...options,
wrapper: withNavigation ? TestProvidersWithNavigation : TestProviders,
});
};

View File

@@ -1,7 +1,6 @@
import { useEffect } from 'react';
import { wsService } from '@/services/ws.service';
import { usePoseStore } from '@/stores/poseStore';
import { useSettingsStore } from '@/stores/settingsStore';
export interface UsePoseStreamResult {
connectionStatus: ReturnType<typeof usePoseStore.getState>['connectionStatus'];
@@ -10,7 +9,6 @@ export interface UsePoseStreamResult {
}
export function usePoseStream(): UsePoseStreamResult {
const serverUrl = useSettingsStore((state) => state.serverUrl);
const connectionStatus = usePoseStore((state) => state.connectionStatus);
const lastFrame = usePoseStore((state) => state.lastFrame);
const isSimulated = usePoseStore((state) => state.isSimulated);
@@ -19,13 +17,11 @@ export function usePoseStream(): UsePoseStreamResult {
const unsubscribe = wsService.subscribe((frame) => {
usePoseStore.getState().handleFrame(frame);
});
wsService.connect(serverUrl);
return () => {
unsubscribe();
wsService.disconnect();
};
}, [serverUrl]);
}, []);
return { connectionStatus, lastFrame, isSimulated };
}

View File

@@ -1,10 +1,11 @@
import React, { Suspense, useEffect, useState } from 'react';
import React, { Suspense } from 'react';
import { ActivityIndicator } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { ThemedText } from '../components/ThemedText';
import { ThemedView } from '../components/ThemedView';
import { colors } from '../theme/colors';
import { useMatStore } from '../stores/matStore';
import { MainTabsParamList } from './types';
const createPlaceholder = (label: string) => {
@@ -74,29 +75,6 @@ const toIconName = (routeName: keyof MainTabsParamList) => {
}
};
const getMatAlertCount = async (): Promise<number> => {
try {
const mod = (await import('../stores/matStore')) as Record<string, unknown>;
const candidates = [mod.useMatStore, mod.useStore].filter((candidate) => {
return (
!!candidate &&
typeof candidate === 'function' &&
typeof (candidate as { getState?: () => unknown }).getState === 'function'
);
}) as Array<{ getState: () => { alerts?: unknown[] } }>;
for (const store of candidates) {
const alerts = store.getState().alerts;
if (Array.isArray(alerts)) {
return alerts.length;
}
}
} catch {
return 0;
}
return 0;
};
const screens: ReadonlyArray<{ name: keyof MainTabsParamList; component: React.ComponentType }> = [
{ name: 'Live', component: LiveScreen },
{ name: 'Vitals', component: VitalsScreen },
@@ -114,18 +92,7 @@ const Suspended = ({ component: Component }: { component: React.ComponentType })
);
export const MainTabs = () => {
const [matAlertCount, setMatAlertCount] = useState(0);
useEffect(() => {
const readCount = async () => {
const count = await getMatAlertCount();
setMatAlertCount(count);
};
void readCount();
const timer = setInterval(readCount, 2000);
return () => clearInterval(timer);
}, []);
const matAlertCount = useMatStore((state) => state.alerts.length);
return (
<Tab.Navigator

View File

@@ -98,10 +98,11 @@ export const SettingsScreen = () => {
const intervalSummary = useMemo(() => `${scanInterval}s`, [scanInterval]);
const handleSaveUrl = () => {
setServerUrl(draftUrl);
apiService.setBaseUrl(draftUrl);
const newUrl = draftUrl.trim();
setServerUrl(newUrl);
wsService.disconnect();
wsService.connect(draftUrl);
wsService.connect(newUrl);
apiService.setBaseUrl(newUrl);
};
const handleOpenGitHub = async () => {