Just stopping now bc PC needs to reboot.
This commit is contained in:
		
							
								
								
									
										213
									
								
								components/status/StatusCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								components/status/StatusCard.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,213 @@
 | 
			
		||||
// components/status/StatusCard.tsx
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { 
 | 
			
		||||
  StyleSheet, 
 | 
			
		||||
  Modal, 
 | 
			
		||||
  TouchableOpacity, 
 | 
			
		||||
  TouchableWithoutFeedback,
 | 
			
		||||
  KeyboardAvoidingView,
 | 
			
		||||
  Platform,
 | 
			
		||||
  View,
 | 
			
		||||
  ActivityIndicator,
 | 
			
		||||
  Alert
 | 
			
		||||
} from 'react-native';
 | 
			
		||||
import { supabase } from '@/lib/supabase';
 | 
			
		||||
import { ThemedView, ThemedText, ThemedTextInput, ThemedTextButton } from '@/components/theme';
 | 
			
		||||
import ProfileAvatar from '@/components/auth/Profile_Avatar';
 | 
			
		||||
 | 
			
		||||
interface StatusCardProps {
 | 
			
		||||
  visible: boolean;
 | 
			
		||||
  user: {
 | 
			
		||||
    user_id: string;
 | 
			
		||||
    status: string;
 | 
			
		||||
    profiles: {
 | 
			
		||||
      full_name: string;
 | 
			
		||||
      avatar_url: string | null;
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
  onClose: () => void;
 | 
			
		||||
  onUpdate: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function StatusCard({ visible, user, onClose, onUpdate }: StatusCardProps) {
 | 
			
		||||
  const [newStatus, setNewStatus] = useState('');
 | 
			
		||||
  const [updating, setUpdating] = useState(false);
 | 
			
		||||
  
 | 
			
		||||
  const handleUpdateStatus = async () => {
 | 
			
		||||
    if (!newStatus.trim() || newStatus.trim().length < 3) {
 | 
			
		||||
      Alert.alert('Invalid Status', 'Status must be at least 3 characters long.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    setUpdating(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const { data: { user: currentUser } } = await supabase.auth.getUser();
 | 
			
		||||
      
 | 
			
		||||
      if (!currentUser) throw new Error('Not authenticated');
 | 
			
		||||
      
 | 
			
		||||
      // Insert new status
 | 
			
		||||
      const { error } = await supabase
 | 
			
		||||
        .from('statuses')
 | 
			
		||||
        .insert({
 | 
			
		||||
          user_id: user.user_id,
 | 
			
		||||
          status: newStatus.trim()
 | 
			
		||||
        });
 | 
			
		||||
      
 | 
			
		||||
      if (error) throw error;
 | 
			
		||||
      
 | 
			
		||||
      setNewStatus('');
 | 
			
		||||
      onUpdate();
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      Alert.alert('Error', error instanceof Error ? error.message : 'Failed to update status');
 | 
			
		||||
      console.error('Status update error:', error);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setUpdating(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      animationType="slide"
 | 
			
		||||
      transparent={true}
 | 
			
		||||
      visible={visible}
 | 
			
		||||
      onRequestClose={onClose}
 | 
			
		||||
    >
 | 
			
		||||
      <TouchableWithoutFeedback onPress={onClose}>
 | 
			
		||||
        <View style={styles.modalOverlay} />
 | 
			
		||||
      </TouchableWithoutFeedback>
 | 
			
		||||
      
 | 
			
		||||
      <KeyboardAvoidingView
 | 
			
		||||
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
 | 
			
		||||
        style={styles.keyboardAvoidingView}
 | 
			
		||||
      >
 | 
			
		||||
        <ThemedView style={styles.modalContent}>
 | 
			
		||||
          <View style={styles.handle} />
 | 
			
		||||
          
 | 
			
		||||
          <View style={styles.userInfoContainer}>
 | 
			
		||||
            <ProfileAvatar
 | 
			
		||||
              url={user.profiles.avatar_url}
 | 
			
		||||
              size={60}
 | 
			
		||||
              disabled={true}
 | 
			
		||||
            />
 | 
			
		||||
            <ThemedText style={styles.userName}>
 | 
			
		||||
              {user.profiles.full_name}
 | 
			
		||||
            </ThemedText>
 | 
			
		||||
            <ThemedText style={styles.currentStatus}>
 | 
			
		||||
              Current: {user.status}
 | 
			
		||||
            </ThemedText>
 | 
			
		||||
          </View>
 | 
			
		||||
          
 | 
			
		||||
          <ThemedView style={styles.inputContainer}>
 | 
			
		||||
            <ThemedText style={styles.inputLabel}>New Status</ThemedText>
 | 
			
		||||
            <ThemedTextInput
 | 
			
		||||
              value={newStatus}
 | 
			
		||||
              onChangeText={setNewStatus}
 | 
			
		||||
              placeholder="What's happening?"
 | 
			
		||||
              maxLength={80}
 | 
			
		||||
              multiline
 | 
			
		||||
              style={styles.input}
 | 
			
		||||
              editable={!updating}
 | 
			
		||||
            />
 | 
			
		||||
            <ThemedText style={styles.charCount}>
 | 
			
		||||
              {newStatus.length}/80
 | 
			
		||||
            </ThemedText>
 | 
			
		||||
          </ThemedView>
 | 
			
		||||
          
 | 
			
		||||
          <ThemedTextButton
 | 
			
		||||
            text={updating ? 'Updating...' : 'Update Status'}
 | 
			
		||||
            onPress={handleUpdateStatus}
 | 
			
		||||
            disabled={updating || newStatus.trim().length < 3}
 | 
			
		||||
            fontSize={18}
 | 
			
		||||
            fontWeight='semibold'
 | 
			
		||||
            width='100%'
 | 
			
		||||
            style={styles.updateButton}
 | 
			
		||||
          />
 | 
			
		||||
          
 | 
			
		||||
          <TouchableOpacity 
 | 
			
		||||
            style={styles.cancelButton} 
 | 
			
		||||
            onPress={onClose}
 | 
			
		||||
            disabled={updating}
 | 
			
		||||
          >
 | 
			
		||||
            <ThemedText style={styles.cancelText}>Cancel</ThemedText>
 | 
			
		||||
          </TouchableOpacity>
 | 
			
		||||
        </ThemedView>
 | 
			
		||||
      </KeyboardAvoidingView>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const styles = StyleSheet.create({
 | 
			
		||||
  modalOverlay: {
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    backgroundColor: 'rgba(0,0,0,0.5)',
 | 
			
		||||
  },
 | 
			
		||||
  keyboardAvoidingView: {
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    bottom: 0,
 | 
			
		||||
    left: 0,
 | 
			
		||||
    right: 0,
 | 
			
		||||
  },
 | 
			
		||||
  modalContent: {
 | 
			
		||||
    borderTopLeftRadius: 20,
 | 
			
		||||
    borderTopRightRadius: 20,
 | 
			
		||||
    padding: 20,
 | 
			
		||||
    paddingBottom: Platform.OS === 'ios' ? 40 : 20,
 | 
			
		||||
  },
 | 
			
		||||
  handle: {
 | 
			
		||||
    width: 40,
 | 
			
		||||
    height: 5,
 | 
			
		||||
    borderRadius: 3,
 | 
			
		||||
    backgroundColor: '#ccc',
 | 
			
		||||
    alignSelf: 'center',
 | 
			
		||||
    marginBottom: 20,
 | 
			
		||||
  },
 | 
			
		||||
  userInfoContainer: {
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    marginBottom: 20,
 | 
			
		||||
  },
 | 
			
		||||
  userName: {
 | 
			
		||||
    fontSize: 18,
 | 
			
		||||
    fontWeight: '600',
 | 
			
		||||
    marginTop: 10,
 | 
			
		||||
  },
 | 
			
		||||
  currentStatus: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    marginTop: 5,
 | 
			
		||||
    opacity: 0.7,
 | 
			
		||||
  },
 | 
			
		||||
  inputContainer: {
 | 
			
		||||
    marginBottom: 20,
 | 
			
		||||
  },
 | 
			
		||||
  inputLabel: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    fontWeight: '500',
 | 
			
		||||
    marginBottom: 8,
 | 
			
		||||
  },
 | 
			
		||||
  input: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    paddingVertical: 12,
 | 
			
		||||
    paddingHorizontal: 10,
 | 
			
		||||
    borderRadius: 8,
 | 
			
		||||
    minHeight: 80,
 | 
			
		||||
    textAlignVertical: 'top',
 | 
			
		||||
  },
 | 
			
		||||
  charCount: {
 | 
			
		||||
    fontSize: 12,
 | 
			
		||||
    alignSelf: 'flex-end',
 | 
			
		||||
    marginTop: 4,
 | 
			
		||||
    opacity: 0.6,
 | 
			
		||||
  },
 | 
			
		||||
  updateButton: {
 | 
			
		||||
    borderRadius: 8,
 | 
			
		||||
    marginBottom: 15,
 | 
			
		||||
  },
 | 
			
		||||
  cancelButton: {
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    padding: 10,
 | 
			
		||||
  },
 | 
			
		||||
  cancelText: {
 | 
			
		||||
    fontSize: 16,
 | 
			
		||||
    color: '#FF3B30',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										543
									
								
								components/status/StatusList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										543
									
								
								components/status/StatusList.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,543 @@
 | 
			
		||||
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<UserStatus[]>([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const [refreshing, setRefreshing] = useState(false);
 | 
			
		||||
  const [selectedUser, setSelectedUser] = useState<UserStatus | null>(null);
 | 
			
		||||
  const [showStatusCard, setShowStatusCard] = useState(false);
 | 
			
		||||
  const [recentlyUpdatedIds, setRecentlyUpdatedIds] = useState<Set<string>>(new Set());
 | 
			
		||||
  const [isConnected, setIsConnected] = useState(true);
 | 
			
		||||
  const [lastFetchTime, setLastFetchTime] = useState<Date | null>(null);
 | 
			
		||||
  
 | 
			
		||||
  const fadeAnimation = useRef(new Animated.Value(0)).current;
 | 
			
		||||
  const isFocused = useIsFocused();
 | 
			
		||||
  const subscriptionRef = useRef<RealtimeChannel | null>(null);
 | 
			
		||||
  const appStateRef = useRef(AppState.currentState);
 | 
			
		||||
  const pendingUpdatesRef = useRef<Set<string>>(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<string>();
 | 
			
		||||
          
 | 
			
		||||
          // 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 (
 | 
			
		||||
      <TouchableOpacity
 | 
			
		||||
        style={[
 | 
			
		||||
          styles.statusItem,
 | 
			
		||||
          recentlyUpdatedIds.has(item.id) && styles.recentlyUpdated,
 | 
			
		||||
        ]}
 | 
			
		||||
        onPress={() => handleUserSelect(item)}
 | 
			
		||||
        activeOpacity={0.7}
 | 
			
		||||
      >
 | 
			
		||||
        <ThemedView style={styles.contentContainer}>
 | 
			
		||||
          <ThemedView style={styles.topRow}>
 | 
			
		||||
            <ThemedView style={styles.userContainer}>
 | 
			
		||||
              <ThemedView style={styles.avatarContainer}>
 | 
			
		||||
                <ProfileAvatar
 | 
			
		||||
                  url={item.profiles.avatar_url}
 | 
			
		||||
                  size={30}
 | 
			
		||||
                  disabled={true}
 | 
			
		||||
                />
 | 
			
		||||
              </ThemedView>
 | 
			
		||||
              <ThemedText
 | 
			
		||||
                type='custom'
 | 
			
		||||
                fontSize={24}
 | 
			
		||||
                fontWeight='bold'
 | 
			
		||||
              >
 | 
			
		||||
                {item.profiles.full_name}
 | 
			
		||||
              </ThemedText>
 | 
			
		||||
            </ThemedView>
 | 
			
		||||
          </ThemedView>
 | 
			
		||||
          <ThemedView style={styles.bottomRow}>
 | 
			
		||||
            <ThemedText type='custom' fontSize={18} style={styles.statusText}>
 | 
			
		||||
              {item.status}
 | 
			
		||||
            </ThemedText>
 | 
			
		||||
          </ThemedView>
 | 
			
		||||
        </ThemedView>
 | 
			
		||||
        <ThemedView style={styles.timeContainer}>
 | 
			
		||||
          <ThemedText type='custom' fontSize={20} fontWeight='semibold'>
 | 
			
		||||
            {formattedDate.time}
 | 
			
		||||
          </ThemedText>
 | 
			
		||||
          <ThemedText type='custom' fontSize={20} fontWeight='500'>
 | 
			
		||||
            {formattedDate.date}
 | 
			
		||||
          </ThemedText>
 | 
			
		||||
        </ThemedView>
 | 
			
		||||
      </TouchableOpacity>
 | 
			
		||||
    );
 | 
			
		||||
  }, [formatDate, handleUserSelect, recentlyUpdatedIds]);
 | 
			
		||||
  
 | 
			
		||||
  // Empty list component
 | 
			
		||||
  const ListEmptyComponent = useCallback(() => (
 | 
			
		||||
    <ThemedView style={styles.emptyContainer}>
 | 
			
		||||
      <ThemedText style={styles.emptyText}>
 | 
			
		||||
        {isConnected ? 'No statuses available' : 'You are offline'}
 | 
			
		||||
      </ThemedText>
 | 
			
		||||
    </ThemedView>
 | 
			
		||||
  ), [isConnected]);
 | 
			
		||||
  
 | 
			
		||||
  if (loading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ThemedView style={styles.loadingContainer}>
 | 
			
		||||
        <ActivityIndicator size="large" />
 | 
			
		||||
      </ThemedView>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return (
 | 
			
		||||
    <ThemedView style={styles.container}>
 | 
			
		||||
      {!isConnected && (
 | 
			
		||||
        <ThemedView style={styles.offlineBar}>
 | 
			
		||||
          <ThemedText style={styles.offlineText}>You are offline</ThemedText>
 | 
			
		||||
        </ThemedView>
 | 
			
		||||
      )}
 | 
			
		||||
      
 | 
			
		||||
      <FlatList
 | 
			
		||||
        data={statuses}
 | 
			
		||||
        keyExtractor={(item) => item.id}
 | 
			
		||||
        refreshControl={
 | 
			
		||||
          <RefreshControl 
 | 
			
		||||
            refreshing={refreshing} 
 | 
			
		||||
            onRefresh={onRefresh}
 | 
			
		||||
            enabled={isConnected}
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
        renderItem={renderItem}
 | 
			
		||||
        ListEmptyComponent={ListEmptyComponent}
 | 
			
		||||
        initialNumToRender={10}
 | 
			
		||||
        maxToRenderPerBatch={10}
 | 
			
		||||
        windowSize={10}
 | 
			
		||||
        removeClippedSubviews={Platform.OS !== 'web'}
 | 
			
		||||
        getItemLayout={(data, index) => (
 | 
			
		||||
          {length: 120, offset: 120 * index, index}
 | 
			
		||||
        )}
 | 
			
		||||
      />
 | 
			
		||||
      
 | 
			
		||||
      {selectedUser && (
 | 
			
		||||
        <StatusCard
 | 
			
		||||
          visible={showStatusCard}
 | 
			
		||||
          user={selectedUser}
 | 
			
		||||
          onClose={() => setShowStatusCard(false)}
 | 
			
		||||
          onUpdate={handleStatusUpdate}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </ThemedView>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user