diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index cd221da..6b6cbf2 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -1,6 +1,7 @@
import { Image, StyleSheet, Platform } from 'react-native';
import ParallaxScrollView from '@/components/default/ParallaxScrollView';
import { ThemedText, ThemedView } from '@/components/theme';
+import StatusList from '@/components/status/StatusList';
const HomeScreen = () => {
return (
@@ -14,7 +15,9 @@ const HomeScreen = () => {
}
>
-
+
+
+
);
};
diff --git a/app/(tabs)/settings/profile.tsx b/app/(tabs)/settings/profile.tsx
index b5aa178..8c25eb7 100644
--- a/app/(tabs)/settings/profile.tsx
+++ b/app/(tabs)/settings/profile.tsx
@@ -105,12 +105,14 @@ const ProfileScreen = () => {
return (
-
+
+
+
{profile.provider && (
@@ -160,6 +162,9 @@ const styles = StyleSheet.create({
padding: 16,
alignItems: 'center',
},
+ avatarContainer: {
+ marginVertical: 20,
+ },
loadingContainer: {
flex: 1,
justifyContent: 'center',
diff --git a/components/auth/Profile_Avatar.tsx b/components/auth/Profile_Avatar.tsx
index 82b455d..abcb7e5 100644
--- a/components/auth/Profile_Avatar.tsx
+++ b/components/auth/Profile_Avatar.tsx
@@ -149,9 +149,13 @@ export default function ProfileAvatar({
{uploading ? (
) : (
-
- {disabled ? 'Avatar' : 'Change Photo'}
-
+ disabled ? (
+
+ ) : (
+
+ Change Photo
+
+ )
)}
);
@@ -160,7 +164,6 @@ export default function ProfileAvatar({
const styles = StyleSheet.create({
avatarContainer: {
alignItems: 'center',
- marginVertical: 20,
},
avatar: {
backgroundColor: '#E1E1E1',
diff --git a/components/status/StatusCard.tsx b/components/status/StatusCard.tsx
new file mode 100644
index 0000000..738752a
--- /dev/null
+++ b/components/status/StatusCard.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ {user.profiles.full_name}
+
+
+ Current: {user.status}
+
+
+
+
+ New Status
+
+
+ {newStatus.length}/80
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/components/status/StatusList.tsx b/components/status/StatusList.tsx
new file mode 100644
index 0000000..71d5ce6
--- /dev/null
+++ b/components/status/StatusList.tsx
@@ -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([]);
+ 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',
+ },
+});
diff --git a/constants/Types.ts b/constants/Types.ts
index c07878a..b66cb85 100644
--- a/constants/Types.ts
+++ b/constants/Types.ts
@@ -1,3 +1,4 @@
+
export type updateUser = {
id?: string;
updated_at?: Date;
@@ -7,6 +8,17 @@ export type updateUser = {
provider?: string;
};
+export type UserStatus = {
+ id: string;
+ user_id: string;
+ status: string;
+ created_at: string;
+ profiles: {
+ full_name: string;
+ avatar_url: string | null;
+ };
+};
+
export type NotificationMessage = {
sound?: string;
title: string;
diff --git a/package-lock.json b/package-lock.json
index 8c8e5f5..67255b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,10 +15,12 @@
"@expo/ngrok": "4.1.0",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
+ "@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@supabase/supabase-js": "^2.48.1",
"aes-js": "^3.1.2",
+ "date-fns": "^4.1.0",
"expo": "~52.0.28",
"expo-apple-authentication": "~7.1.3",
"expo-auth-session": "~6.0.3",
@@ -45,6 +47,7 @@
"expo-web-browser": "~14.0.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
+ "lodash": "^4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
@@ -61,6 +64,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
+ "@types/lodash": "^4.17.16",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1",
@@ -3813,6 +3817,15 @@
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
+ "node_modules/@react-native-community/netinfo": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
+ "integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react-native": ">=0.59"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.76.6",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
@@ -5281,6 +5294,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.16",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
+ "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
@@ -7328,6 +7348,16 @@
"node": ">=12"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
diff --git a/package.json b/package.json
index 5e71a06..cbad66c 100644
--- a/package.json
+++ b/package.json
@@ -24,10 +24,12 @@
"@expo/ngrok": "4.1.0",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
+ "@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@supabase/supabase-js": "^2.48.1",
"aes-js": "^3.1.2",
+ "date-fns": "^4.1.0",
"expo": "~52.0.28",
"expo-apple-authentication": "~7.1.3",
"expo-auth-session": "~6.0.3",
@@ -38,6 +40,8 @@
"expo-font": "~13.0.3",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.6",
+ "expo-image-manipulator": "~13.0.6",
+ "expo-image-picker": "~16.0.6",
"expo-insights": "~0.8.2",
"expo-linking": "~7.0.5",
"expo-location": "~18.0.7",
@@ -52,6 +56,7 @@
"expo-web-browser": "~14.0.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
+ "lodash": "^4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
@@ -63,13 +68,12 @@
"react-native-svg": "15.8.0",
"react-native-svg-transformer": "^1.5.0",
"react-native-web": "~0.19.13",
- "react-native-webview": "13.12.5",
- "expo-image-picker": "~16.0.6",
- "expo-image-manipulator": "~13.0.6"
+ "react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
+ "@types/lodash": "^4.17.16",
"@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1",
diff --git a/scripts/supabase_schema.sql b/scripts/supabase_schema.sql
index 74ada70..253d361 100644
--- a/scripts/supabase_schema.sql
+++ b/scripts/supabase_schema.sql
@@ -7,7 +7,7 @@ create table profiles (
avatar_url text,
provider text,
- constraint full_name_length check (char_length(full_name) >= 3 and char_length(full_name) <= 50),
+ constraint full_name_length check (char_length(full_name) >= 3 and char_length(full_name) <= 50)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
@@ -57,3 +57,47 @@ create policy "Avatar images are publicly accessible." on storage.objects
create policy "Anyone can upload an avatar." on storage.objects
for insert with check (bucket_id = 'avatars');
+
+
+-- Create a table for public statuses
+CREATE TABLE statuses (
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
+ user_id uuid REFERENCES auth.users ON DELETE CASCADE NOT NULL,
+ created_at timestamp with time zone DEFAULT now() NOT NULL,
+ status text NOT NULL,
+ CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80),
+ CONSTRAINT statuses_user_id_fkey FOREIGN KEY (user_id) REFERENCES profiles(id) ON DELETE CASCADE
+);
+
+-- Set up Row Level Security (RLS)
+ALTER TABLE statuses
+ ENABLE ROW LEVEL SECURITY;
+
+-- Policies
+CREATE POLICY "Public statuses are viewable by everyone." ON statuses
+ FOR SELECT USING (true);
+
+CREATE POLICY "Users can insert their own statuses." ON statuses
+ FOR INSERT WITH CHECK ((SELECT auth.uid()) = user_id);
+
+-- Function to add first status
+CREATE FUNCTION public.handle_first_status()
+RETURNS TRIGGER
+SET search_path = ''
+AS $$
+BEGIN
+ INSERT INTO public.statuses (user_id, status)
+ VALUES (
+ NEW.id,
+ 'Just joined!'
+ );
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- Create a separate trigger for the status
+CREATE TRIGGER on_auth_user_created_add_status
+ AFTER INSERT ON auth.users
+ FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status();
+
+alter publication supabase_realtime add table statuses;