WellNuo/app/(tabs)/dashboard.tsx
Sergei a589401158 Add pull-to-refresh with loading states
Implemented pull-to-refresh functionality across all main screens:

- Home screen: Added RefreshControl to beneficiaries FlatList
  - Separated isLoading (initial load) from isRefreshing (pull-to-refresh)
  - Only show full screen spinner on initial load, not during refresh
  - Pass isRefresh flag to loadBeneficiaries to control loading state

- Chat screen: Added RefreshControl to messages FlatList
  - Reset conversation to initial welcome message on refresh
  - Stop TTS and voice input during refresh to prevent conflicts
  - Clear state cleanly before resetting messages

- Profile screen: Added RefreshControl to ScrollView
  - Reload avatar from cloud/cache on refresh
  - Graceful error handling if avatar load fails

- Dashboard screen: Enhanced visual feedback on refresh
  - Show ActivityIndicator in refresh button when refreshing
  - Disable refresh button during refresh to prevent rapid-fire
  - Reset isRefreshing state on WebView load completion

Added comprehensive tests (23 test cases) covering:
- RefreshControl integration on all screens
- Loading state differentiation (isLoading vs isRefreshing)
- Error handling during refresh
- User experience (platform colors, visual feedback)
- Integration verification for all implementations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 16:28:40 -08:00

206 lines
5.7 KiB
TypeScript

import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
import { WebView } from 'react-native-webview';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
import { FullScreenError } from '@/components/ui/ErrorMessage';
import { useDebounce } from '@/hooks/useDebounce';
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
// BUILD VERSION INDICATOR - видно на экране для проверки версии
const BUILD_VERSION = 'v2.1.0';
const BUILD_TIMESTAMP = '2026-01-27 17:05';
export default function DashboardScreen() {
const webViewRef = useRef<WebView>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [canGoBack, setCanGoBack] = useState(false);
const handleRefreshInternal = () => {
setError(null);
setIsRefreshing(true);
setIsLoading(true);
webViewRef.current?.reload();
};
const { debouncedFn: handleRefresh } = useDebounce(handleRefreshInternal, { delay: 1000 });
const handleBack = () => {
if (canGoBack) {
webViewRef.current?.goBack();
}
};
const handleNavigationStateChange = (navState: any) => {
setCanGoBack(navState.canGoBack);
};
const handleError = () => {
setError('Failed to load dashboard. Please check your internet connection.');
setIsLoading(false);
setIsRefreshing(false);
};
if (error) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Dashboard</Text>
</View>
<FullScreenError message={error} onRetry={handleRefresh} />
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
{canGoBack && (
<TouchableOpacity style={styles.backButton} onPress={handleBack}>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
)}
<Text style={[styles.headerTitle, canGoBack && styles.headerTitleWithBack]}>
Dashboard
</Text>
<TouchableOpacity
style={styles.refreshButton}
onPress={handleRefresh}
disabled={isRefreshing}
>
{isRefreshing ? (
<ActivityIndicator size="small" color={AppColors.primary} />
) : (
<Ionicons name="refresh" size={22} color={AppColors.primary} />
)}
</TouchableOpacity>
</View>
{/* Build Version Indicator - ALWAYS VISIBLE */}
<View style={styles.buildVersionBadge}>
<Text style={styles.buildVersionText}>
{BUILD_VERSION} {BUILD_TIMESTAMP}
</Text>
</View>
{/* WebView */}
<View style={styles.webViewContainer}>
<WebView
ref={webViewRef}
source={{ uri: DASHBOARD_URL }}
style={styles.webView}
onLoadStart={() => setIsLoading(true)}
onLoadEnd={() => {
setIsLoading(false);
setIsRefreshing(false);
}}
onError={handleError}
onHttpError={handleError}
onNavigationStateChange={handleNavigationStateChange}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
scalesPageToFit={true}
allowsBackForwardNavigationGestures={true}
renderLoading={() => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading dashboard...</Text>
</View>
)}
/>
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={AppColors.primary} />
</View>
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
marginRight: Spacing.sm,
},
headerTitle: {
flex: 1,
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
},
headerTitleWithBack: {
marginLeft: 0,
},
refreshButton: {
padding: Spacing.xs,
},
webViewContainer: {
flex: 1,
},
webView: {
flex: 1,
},
loadingContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: AppColors.background,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.8)',
},
loadingText: {
marginTop: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
buildVersionBadge: {
position: 'absolute',
bottom: 60,
right: 10,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
zIndex: 9999,
},
buildVersionText: {
color: '#00FF00',
fontSize: 10,
fontWeight: '600',
fontFamily: 'monospace',
},
});