import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Animated, StyleSheet, FlatList, TouchableOpacity, RefreshControl, ActivityIndicator, AppState, AppStateStatus, Platform, } from 'react-native'; import { supabase } from '@/lib/supabase'; import { ThemedView, ThemedText } from '@/components/theme'; import { formatDistanceToNow } from 'date-fns'; import StatusCard from './StatusCard'; import ProfileAvatar from '@/components/auth/Profile_Avatar'; import { useIsFocused } from '@react-navigation/native'; import { RealtimeChannel } from '@supabase/supabase-js'; import { UserStatus } from '@/constants/Types'; import debounce from 'lodash/debounce'; import NetInfo from '@react-native-community/netinfo'; const StatusList = () => { const [statuses, setStatuses] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [showStatusCard, setShowStatusCard] = useState(false); const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState>(new Set()); const [isConnected, setIsConnected] = useState(true); const [lastFetchTime, setLastFetchTime] = useState(null); const fadeAnimation = useRef(new Animated.Value(0)).current; const isFocused = useIsFocused(); const subscriptionRef = useRef(null); const appStateRef = useRef(AppState.currentState); const pendingUpdatesRef = useRef>(new Set()); // Debounced version of the status update handler const debouncedHandleStatusUpdates = useRef( debounce(() => { if (pendingUpdatesRef.current.size > 0) { const statusesToFetch = Array.from(pendingUpdatesRef.current); pendingUpdatesRef.current.clear(); // Fetch all pending status updates in a single query fetchMultipleStatuses(statusesToFetch); } }, 500) ).current; // Fetch multiple statuses at once const fetchMultipleStatuses = async (statusIds: string[]) => { if (statusIds.length === 0) return; try { const { data, error } = await supabase .from('statuses') .select(` id, user_id, status, created_at, profiles:profiles(full_name, avatar_url) `) .in('id', statusIds); if (error) throw error; if (data && data.length > 0) { // Transform the data const transformedData = data.map(item => ({ ...item, profiles: Array.isArray(item.profiles) ? item.profiles[0] : item.profiles })); // Update statuses setStatuses(prevStatuses => { const newStatuses = [...prevStatuses]; const updatedIds = new Set(); // Process each new status transformedData.forEach(newStatus => { const existingIndex = newStatuses.findIndex(s => s.user_id === newStatus.user_id); if (existingIndex !== -1) { // If the new status is more recent, replace the existing one if (new Date(newStatus.created_at) > new Date(newStatuses[existingIndex].created_at)) { newStatuses[existingIndex] = newStatus; updatedIds.add(newStatus.id); } } else { // If this is a new user, add to the array newStatuses.push(newStatus); updatedIds.add(newStatus.id); } }); // Sort by most recent newStatuses.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); // Mark updated statuses for highlighting if (updatedIds.size > 0) { setRecentlyUpdatedIds(prev => { const newSet = new Set(prev); updatedIds.forEach(id => newSet.add(id)); // Schedule removal of highlights setTimeout(() => { setRecentlyUpdatedIds(current => { const updatedSet = new Set(current); updatedIds.forEach(id => updatedSet.delete(id)); return updatedSet; }); }, 3000); return newSet; }); // Animate the fade-in Animated.sequence([ Animated.timing(fadeAnimation, { toValue: 1, duration: 300, useNativeDriver: true, }), Animated.timing(fadeAnimation, { toValue: 0, duration: 300, delay: 2000, useNativeDriver: true, }), ]).start(); } return newStatuses; }); } } catch (error) { console.error('Error fetching multiple statuses:', error); } }; // Fetch statuses with time filtering const fetchStatuses = useCallback(async (forceRefresh = false) => { try { if (!isConnected) { console.log('Skipping fetch - device is offline'); setLoading(false); setRefreshing(false); return; } // Get current time const now = new Date(); setLastFetchTime(now); // Calculate time filter - only get statuses from the last week // unless it's a force refresh let query = supabase .from('statuses') .select(` id, user_id, status, created_at, profiles:profiles(full_name, avatar_url) `) .order('created_at', { ascending: false }); if (!forceRefresh) { const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); query = query.gte('created_at', oneWeekAgo.toISOString()); } const { data, error } = await query; if (error) throw error; if (data) { // Transform the data to match our expected type const transformedData = data.map(item => ({ ...item, profiles: Array.isArray(item.profiles) ? item.profiles[0] : item.profiles })); // Get unique users with their latest status const userMap = new Map(); transformedData.forEach(status => { if (!userMap.has(status.user_id) || new Date(status.created_at) > new Date(userMap.get(status.user_id).created_at)) { userMap.set(status.user_id, status); } }); // Convert map to array and sort by most recent const latestUserStatuses = Array.from(userMap.values()); latestUserStatuses.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() ); setStatuses(latestUserStatuses); } } catch (error) { console.error('Error fetching statuses:', error); } finally { setLoading(false); setRefreshing(false); } }, [isConnected]); // Handle individual status update const handleNewStatus = useCallback((statusId: string) => { // Add to pending updates pendingUpdatesRef.current.add(statusId); // Trigger the debounced handler debouncedHandleStatusUpdates(); }, [debouncedHandleStatusUpdates]); // Set up network connectivity listener useEffect(() => { const unsubscribe = NetInfo.addEventListener(state => { setIsConnected(state.isConnected ?? false); // If we're coming back online and we have a last fetch time // that's more than 1 minute old, refresh the data if (state.isConnected && lastFetchTime) { const now = new Date(); const timeDiff = now.getTime() - lastFetchTime.getTime(); if (timeDiff > 60000) { // 1 minute fetchStatuses(); } } }); return () => { unsubscribe(); }; }, [fetchStatuses, lastFetchTime]); // Set up AppState listener for background/foreground transitions useEffect(() => { const handleAppStateChange = (nextAppState: AppStateStatus) => { if ( appStateRef.current.match(/inactive|background/) && nextAppState === 'active' ) { console.log('App has come to the foreground!'); // Refresh data if we've been in the background for a while if (lastFetchTime) { const now = new Date(); const timeDiff = now.getTime() - lastFetchTime.getTime(); if (timeDiff > 60000) { // 1 minute fetchStatuses(); } } // Reconnect to realtime if needed if (!subscriptionRef.current && isFocused) { setupRealtimeSubscription(); } } appStateRef.current = nextAppState; }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription.remove(); }; }, [fetchStatuses, isFocused, lastFetchTime]); // Set up realtime subscription const setupRealtimeSubscription = useCallback(() => { // Get only statuses from the last week to reduce payload const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); const subscription = supabase .channel('status_changes') .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'statuses', filter: `created_at>gt.${oneWeekAgo.toISOString()}` }, (payload) => { console.log('New status received:', payload); handleNewStatus(payload.new.id); } ) .subscribe((status) => { console.log('Realtime subscription status:', status); }); subscriptionRef.current = subscription; return subscription; }, [handleNewStatus]); // Set up real-time subscription when component is focused useEffect(() => { if (isFocused && isConnected) { // Initial fetch fetchStatuses(); // Set up real-time subscription const subscription = setupRealtimeSubscription(); // Clean up subscription when component unmounts or loses focus return () => { if (subscription) { supabase.removeChannel(subscription); subscriptionRef.current = null; } }; } }, [isFocused, isConnected, fetchStatuses, setupRealtimeSubscription]); // Handle refresh const onRefresh = useCallback(() => { setRefreshing(true); fetchStatuses(true); // Force refresh to get all statuses }, [fetchStatuses]); const handleUserSelect = useCallback((user: UserStatus) => { setSelectedUser(user); setShowStatusCard(true); }, []); const handleStatusUpdate = useCallback(() => { setShowStatusCard(false); // No need to manually fetch statuses here since the real-time subscription will handle it }, []); const formatDate = useCallback((dateString: string) => { const date = new Date(dateString); return { time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), date: date.toLocaleDateString([], { month: 'short', day: 'numeric' }), relative: formatDistanceToNow(date, { addSuffix: true }) }; }, []); // Memoize the list item renderer for better performance const renderItem = useCallback(({ item }: { item: UserStatus }) => { const formattedDate = formatDate(item.created_at); return ( handleUserSelect(item)} activeOpacity={0.7} > {item.profiles.full_name} {item.status} {formattedDate.time} {formattedDate.date} ); }, [formatDate, handleUserSelect, recentlyUpdatedIds]); // Empty list component const ListEmptyComponent = useCallback(() => ( {isConnected ? 'No statuses available' : 'You are offline'} ), [isConnected]); if (loading) { return ( ); } return ( {!isConnected && ( You are offline )} item.id} refreshControl={ } renderItem={renderItem} ListEmptyComponent={ListEmptyComponent} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={10} removeClippedSubviews={Platform.OS !== 'web'} getItemLayout={(data, index) => ( {length: 120, offset: 120 * index, index} )} /> {selectedUser && ( setShowStatusCard(false)} onUpdate={handleStatusUpdate} /> )} ); }; export default React.memo(StatusList); const styles = StyleSheet.create({ container: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, statusItem: { flexDirection: 'row', backgroundColor: 'rgba(200, 200, 200, 0.1)', padding: 16, borderRadius: 12, marginHorizontal: 16, marginVertical: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2, height: 104, // Fixed height for getItemLayout optimization }, recentlyUpdated: { backgroundColor: 'rgba(100, 200, 255, 0.1)', borderLeftWidth: 3, borderLeftColor: '#4C9EFF', }, userContainer: { flex: 1, flexDirection: 'row', alignItems: 'center', backgroundColor: 'transparent', }, avatarContainer: { marginRight: 4, backgroundColor: 'transparent', }, contentContainer: { flex: 1, justifyContent: 'space-between', backgroundColor: 'transparent', }, topRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'transparent', }, timeContainer: { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: 'transparent', }, bottomRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', backgroundColor: 'transparent', }, statusText: { flex: 1, marginLeft: 8, marginRight: 4, }, emptyContainer: { padding: 20, alignItems: 'center', }, emptyText: { fontSize: 16, opacity: 0.6, }, offlineBar: { backgroundColor: '#FF3B30', padding: 8, alignItems: 'center', }, offlineText: { color: 'white', fontWeight: 'bold', }, });