Just stopping now bc PC needs to reboot.
This commit is contained in:
parent
cfcf118275
commit
0cdfd1a0eb
@ -1,6 +1,7 @@
|
|||||||
import { Image, StyleSheet, Platform } from 'react-native';
|
import { Image, StyleSheet, Platform } from 'react-native';
|
||||||
import ParallaxScrollView from '@/components/default/ParallaxScrollView';
|
import ParallaxScrollView from '@/components/default/ParallaxScrollView';
|
||||||
import { ThemedText, ThemedView } from '@/components/theme';
|
import { ThemedText, ThemedView } from '@/components/theme';
|
||||||
|
import StatusList from '@/components/status/StatusList';
|
||||||
|
|
||||||
const HomeScreen = () => {
|
const HomeScreen = () => {
|
||||||
return (
|
return (
|
||||||
@ -14,7 +15,9 @@ const HomeScreen = () => {
|
|||||||
</ThemedText>
|
</ThemedText>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ThemedView style={styles.titleContainer}></ThemedView>
|
<ThemedView style={styles.titleContainer}>
|
||||||
|
<StatusList />
|
||||||
|
</ThemedView>
|
||||||
</ParallaxScrollView>
|
</ParallaxScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -105,12 +105,14 @@ const ProfileScreen = () => {
|
|||||||
return (
|
return (
|
||||||
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
<ScrollView contentContainerStyle={styles.scrollContainer}>
|
||||||
<ThemedView style={styles.container}>
|
<ThemedView style={styles.container}>
|
||||||
<ProfileAvatar
|
<ThemedView style={styles.avatarContainer}>
|
||||||
url={profile.avatar_url}
|
<ProfileAvatar
|
||||||
size={120}
|
url={profile.avatar_url}
|
||||||
onUpload={handleAvatarUpload}
|
size={120}
|
||||||
disabled={updating}
|
onUpload={handleAvatarUpload}
|
||||||
/>
|
disabled={updating}
|
||||||
|
/>
|
||||||
|
</ThemedView>
|
||||||
|
|
||||||
{profile.provider && (
|
{profile.provider && (
|
||||||
<ThemedText style={styles.providerText}>
|
<ThemedText style={styles.providerText}>
|
||||||
@ -160,6 +162,9 @@ const styles = StyleSheet.create({
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
avatarContainer: {
|
||||||
|
marginVertical: 20,
|
||||||
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
@ -149,9 +149,13 @@ export default function ProfileAvatar({
|
|||||||
{uploading ? (
|
{uploading ? (
|
||||||
<ActivityIndicator style={styles.uploadingIndicator} size="small" color="#007AFF" />
|
<ActivityIndicator style={styles.uploadingIndicator} size="small" color="#007AFF" />
|
||||||
) : (
|
) : (
|
||||||
<ThemedText style={styles.changePhotoText}>
|
disabled ? (
|
||||||
{disabled ? 'Avatar' : 'Change Photo'}
|
<ThemedView />
|
||||||
</ThemedText>
|
) : (
|
||||||
|
<ThemedText style={styles.changePhotoText}>
|
||||||
|
Change Photo
|
||||||
|
</ThemedText>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
@ -160,7 +164,6 @@ export default function ProfileAvatar({
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
avatarContainer: {
|
avatarContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginVertical: 20,
|
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
backgroundColor: '#E1E1E1',
|
backgroundColor: '#E1E1E1',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
export type updateUser = {
|
export type updateUser = {
|
||||||
id?: string;
|
id?: string;
|
||||||
updated_at?: Date;
|
updated_at?: Date;
|
||||||
@ -7,6 +8,17 @@ export type updateUser = {
|
|||||||
provider?: string;
|
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 = {
|
export type NotificationMessage = {
|
||||||
sound?: string;
|
sound?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
30
package-lock.json
generated
30
package-lock.json
generated
@ -15,10 +15,12 @@
|
|||||||
"@expo/ngrok": "4.1.0",
|
"@expo/ngrok": "4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
"@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/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@supabase/supabase-js": "^2.48.1",
|
"@supabase/supabase-js": "^2.48.1",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.28",
|
||||||
"expo-apple-authentication": "~7.1.3",
|
"expo-apple-authentication": "~7.1.3",
|
||||||
"expo-auth-session": "~6.0.3",
|
"expo-auth-session": "~6.0.3",
|
||||||
@ -45,6 +47,7 @@
|
|||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.6",
|
"react-native": "0.76.6",
|
||||||
@ -61,6 +64,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^18.3.0",
|
"@types/react-test-renderer": "^18.3.0",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
@ -3813,6 +3817,15 @@
|
|||||||
"react-native": "^0.0.0-0 || >=0.60 <1.0"
|
"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": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.76.6",
|
"version": "0.76.6",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.6.tgz",
|
||||||
@ -5281,6 +5294,13 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.13.9",
|
"version": "22.13.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
|
||||||
@ -7328,6 +7348,16 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
10
package.json
10
package.json
@ -24,10 +24,12 @@
|
|||||||
"@expo/ngrok": "4.1.0",
|
"@expo/ngrok": "4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
"@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/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@supabase/supabase-js": "^2.48.1",
|
"@supabase/supabase-js": "^2.48.1",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.28",
|
||||||
"expo-apple-authentication": "~7.1.3",
|
"expo-apple-authentication": "~7.1.3",
|
||||||
"expo-auth-session": "~6.0.3",
|
"expo-auth-session": "~6.0.3",
|
||||||
@ -38,6 +40,8 @@
|
|||||||
"expo-font": "~13.0.3",
|
"expo-font": "~13.0.3",
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.6",
|
"expo-image": "~2.0.6",
|
||||||
|
"expo-image-manipulator": "~13.0.6",
|
||||||
|
"expo-image-picker": "~16.0.6",
|
||||||
"expo-insights": "~0.8.2",
|
"expo-insights": "~0.8.2",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
"expo-location": "~18.0.7",
|
"expo-location": "~18.0.7",
|
||||||
@ -52,6 +56,7 @@
|
|||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.6",
|
"react-native": "0.76.6",
|
||||||
@ -63,13 +68,12 @@
|
|||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-webview": "13.12.5",
|
"react-native-webview": "13.12.5"
|
||||||
"expo-image-picker": "~16.0.6",
|
|
||||||
"expo-image-manipulator": "~13.0.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/react": "~18.3.12",
|
"@types/react": "~18.3.12",
|
||||||
"@types/react-test-renderer": "^18.3.0",
|
"@types/react-test-renderer": "^18.3.0",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.1",
|
||||||
|
@ -7,7 +7,7 @@ create table profiles (
|
|||||||
avatar_url text,
|
avatar_url text,
|
||||||
provider 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)
|
-- Set up Row Level Security (RLS)
|
||||||
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
|
-- 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
|
create policy "Anyone can upload an avatar." on storage.objects
|
||||||
for insert with check (bucket_id = 'avatars');
|
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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user