Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 45 additions & 18 deletions packages/mobile-client/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View, SafeAreaView } from 'react-native';
import { StyleSheet, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider as PaperProvider, BottomNavigation, Badge } from 'react-native-paper';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as Haptics from 'expo-haptics';
import type { InjectPromptResponse } from '@codelink/protocol';

// Components
Expand Down Expand Up @@ -40,6 +40,12 @@ import { useCustomFonts } from './src/design-system/typography/fontLoading';

// Utils
import { isInjectPromptResponse, isSyncFullContextMessage } from './src/utils/messageValidation';
import {
getStatusBarStyle,
registerBackButtonHandler,
isAndroid,
triggerHapticFeedback,
} from './src/utils/platformAdaptations';

/**
* Main application content with navigation
Expand Down Expand Up @@ -94,11 +100,11 @@ const AppContent: React.FC = () => {
});
}

// Haptic feedback
// Haptic feedback (Requirements 22.3, 22.4)
if (response.payload.success) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
triggerHapticFeedback('success');
} else {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
triggerHapticFeedback('error');
}

// Clear response after 4 seconds
Expand Down Expand Up @@ -129,17 +135,36 @@ const AppContent: React.FC = () => {
} catch (error) {
setIsSubmitting(false);
setPromptError(error instanceof Error ? error.message : 'Failed to submit prompt');
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
triggerHapticFeedback('error');
}
};

// Handle reconnection (currently unused but kept for future use)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _handleReconnect = async () => {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
await triggerHapticFeedback('medium');
reconnect();
};

// Handle Android hardware back button
// Requirement 26.7: Android hardware back button support
useEffect(() => {
if (!isAndroid()) return;

const backHandler = () => {
// If not on dashboard, go back to dashboard
if (index !== 0) {
setIndex(0);
return true; // Prevent default back behavior
}
// If on dashboard, allow default behavior (exit app)
return false;
};

const cleanup = registerBackButtonHandler(backHandler);
return cleanup;
}, [index]);

// Define navigation routes
const [routes] = useState([
{
Expand Down Expand Up @@ -177,7 +202,7 @@ const AppContent: React.FC = () => {
onNavigateToCompose={() => setIndex(1)}
onNavigateToDiffs={() => setIndex(2)}
onRefresh={async () => {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
await triggerHapticFeedback('light');
}}
/>
</View>
Expand Down Expand Up @@ -247,16 +272,18 @@ const AppContent: React.FC = () => {
};

return (
<SafeAreaView style={[styles.container, { backgroundColor: theme.colors.background }]}>
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
renderIcon={renderIcon}
barStyle={{ backgroundColor: theme.colors.surface }}
/>
<StatusBar style={isDark ? 'light' : 'dark'} />
</SafeAreaView>
<SafeAreaProvider>
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
renderIcon={renderIcon}
barStyle={{ backgroundColor: theme.colors.surface }}
/>
<StatusBar style={getStatusBarStyle(isDark)} />
</View>
</SafeAreaProvider>
);
};

Expand Down
14 changes: 14 additions & 0 deletions packages/mobile-client/metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ config.resolver.nodeModulesPaths = [
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;

// 4. Disable HMR for web to avoid errors
config.server = {
...config.server,
enhanceMiddleware: (middleware) => {
return (req, res, next) => {
// Disable HMR for web platform
if (req.url && req.url.includes('platform=web')) {
req.url = req.url.replace(/&hot=true/g, '&hot=false');
}
return middleware(req, res, next);
};
},
};

module.exports = config;
9 changes: 7 additions & 2 deletions packages/mobile-client/src/components/AppLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
*
* Displays a loading screen while the app initializes (e.g., loading fonts).
*
* Requirements: 2.5
* Requirements: 2.5, 26.8, 26.9
*/

import React from 'react';
import { View, ActivityIndicator, StyleSheet, Text } from 'react-native';
import { getActivityIndicatorSize } from '../utils/platformAdaptations';

export interface AppLoadingProps {
message?: string;
Expand All @@ -18,11 +19,15 @@ export interface AppLoadingProps {
*
* Shows a centered activity indicator with optional message
* while the app is initializing.
*
* Uses platform-specific activity indicator styles:
* - iOS: UIActivityIndicatorView style
* - Android: Material Design CircularProgressIndicator style
*/
export const AppLoading: React.FC<AppLoadingProps> = ({ message = 'Loading...' }) => {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#95ccff" />
<ActivityIndicator size={getActivityIndicatorSize()} color="#95ccff" />
<Text style={styles.message}>{message}</Text>
</View>
);
Expand Down
34 changes: 17 additions & 17 deletions packages/mobile-client/src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, ScrollView, FlatList, Dimensions, RefreshControl } from 'react-native';
import React, { useState } from 'react';
import { View, StyleSheet, ScrollView, FlatList, RefreshControl } from 'react-native';
import { useDesignSystem } from '../design-system';
import { Text } from '../design-system/typography/Text';
import { Card } from '../design-system/components/Card';
Expand All @@ -9,6 +9,7 @@ import { ProgressBar } from '../design-system/components/ProgressBar';
import { StatusIndicator, ConnectionStatus } from '../design-system/components/StatusIndicator';
import { TopAppBar } from '../navigation/TopAppBar';
import { useLoadingAnnouncement } from '../hooks/useScreenReaderAnnouncement';
import { useResponsiveLayout } from '../hooks/useResponsiveLayout';

/**
* System metrics interface
Expand Down Expand Up @@ -79,25 +80,16 @@ export const Dashboard: React.FC<DashboardProps> = ({
onRefresh,
}) => {
const { theme } = useDesignSystem();
const [isLargeScreen, setIsLargeScreen] = useState(false);
const [refreshing, setRefreshing] = useState(false);

// Get responsive layout configuration
// Requirements 13.1, 13.2, 13.5, 13.6, 13.9
const layout = useResponsiveLayout();

// Announce loading state for accessibility
// Requirement 14.11: Announce loading states
useLoadingAnnouncement(refreshing, 'Refreshing dashboard data', 'Dashboard refreshed');

// Detect screen size for responsive layout (Requirement 13.5)
useEffect(() => {
const updateLayout = () => {
const { width } = Dimensions.get('window');
setIsLargeScreen(width >= 768);
};

updateLayout();
const subscription = Dimensions.addEventListener('change', updateLayout);
return () => subscription?.remove();
}, []);

/**
* Handle refresh (Requirement 3.1)
*/
Expand Down Expand Up @@ -239,12 +231,20 @@ export const Dashboard: React.FC<DashboardProps> = ({
</Text>

{/* Bento Grid Layout (Requirement 3.8, 13.6) */}
<View style={[styles.bentoGrid, isLargeScreen && styles.bentoGridLarge]}>
<View
style={[
styles.bentoGrid,
{
flexDirection: layout.bentoGrid.columns === 1 ? 'column' : 'row',
gap: layout.bentoGrid.gap,
},
]}
>
{/* Large card: Uptime (Requirement 3.3) */}
<Card
variant="low"
padding="lg"
style={[styles.bentoCard, isLargeScreen && styles.bentoCardLarge]}
style={[styles.bentoCard, { flex: layout.bentoGrid.largeCardSpan }]}
>
<Text variant="label-sm" color="onSurfaceVariant" uppercase style={styles.cardLabel}>
Uptime
Expand Down
Loading
Loading