Refactor profile screen and organize project structure

- Extract ProfileScreen and components to features/profile
- Move shared hooks and stores to top-level
- Add RLS policies for admin permissions
- Add RELEASING.md guide
- Update .gitignore
This commit is contained in:
Jamie Brunton committed 2026-01-01 23:03:43 +00:00
1 parent df4c4d486b
commit 8f8c6e401d
79 files changed
+6689 -4944

No files matched your search

@@ -0,0 +1,158 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { supabase } from '../lib/supabase';
import { Database } from '../types/supabase';
type Message = Database['public']['Tables']['messages']['Row'];
export interface UseMessageSubscriptionConfig {
/** Match ID to subscribe to */
matchId: string | undefined;
/** Current user's ID */
userId: string | undefined;
/** Callback when a new message is received (e.g., to clear typing indicator) */
onNewMessage?: () => void;
}
export interface UseMessageSubscriptionReturn {
/** Array of messages, sorted newest first */
messages: Message[];
/** Set messages externally (e.g., for optimistic updates) */
setMessages: React.Dispatch<React.SetStateAction<Message[]>>;
/** Whether the initial fetch is in progress */
loading: boolean;
/** Ref to track if screen is focused (for read receipts) */
isFocusedRef: React.MutableRefObject<boolean>;
}
/**
* Hook for managing message subscriptions and real-time updates.
* Handles fetching messages, subscribing to new messages, and managing read receipts.
*
* @param config - Configuration including match ID and user ID
* @returns Message state and subscription utilities
*/
export const useMessageSubscription = (
config: UseMessageSubscriptionConfig
): UseMessageSubscriptionReturn => {
const { matchId, userId, onNewMessage } = config;
const [messages, setMessages] = useState<Message[]>([]);
const [loading, setLoading] = useState(true);
const isFocusedRef = useRef(false);
// Fetch messages and mark as read
useEffect(() => {
if (!matchId || !userId) {
setLoading(false);
return;
}
const fetchMessagesAndMarkRead = async () => {
setLoading(true);
const { data } = await supabase
.from('messages')
.select('*')
.eq('match_id', matchId)
.order('created_at', { ascending: false });
if (data) {
setMessages(data);
const now = new Date().toISOString();
// Get partner messages that need to be marked as delivered or read
const partnerMessages = data.filter(m => m.user_id !== userId);
const undeliveredIds = partnerMessages.filter(m => !m.delivered_at).map(m => m.id);
const unreadIds = partnerMessages.filter(m => !m.read_at).map(m => m.id);
// Mark as both delivered and read (since user is viewing the chat)
if (unreadIds.length > 0) {
await supabase
.from('messages')
.update({ delivered_at: now, read_at: now })
.in('id', unreadIds);
setMessages(prev => prev.map(m =>
unreadIds.includes(m.id) ? { ...m, delivered_at: now, read_at: now } : m
));
} else if (undeliveredIds.length > 0) {
// Only mark as delivered if already read but not delivered (edge case)
await supabase
.from('messages')
.update({ delivered_at: now })
.in('id', undeliveredIds);
setMessages(prev => prev.map(m =>
undeliveredIds.includes(m.id) ? { ...m, delivered_at: now } : m
));
}
}
setLoading(false);
};
fetchMessagesAndMarkRead();
}, [matchId, userId]);
// Subscribe to real-time message updates
useEffect(() => {
if (!matchId || !userId) return;
const channel = supabase
.channel(`messages:${matchId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `match_id=eq.${matchId}`,
},
async (payload) => {
const newMessage = payload.new as Message;
const now = new Date().toISOString();
if (newMessage.user_id !== userId && isFocusedRef.current) {
// Mark as both delivered and read (only if screen is focused)
await supabase
.from('messages')
.update({ delivered_at: now, read_at: now })
.eq('id', newMessage.id);
newMessage.delivered_at = now;
newMessage.read_at = now;
}
setMessages(prev => [newMessage, ...prev]);
onNewMessage?.();
}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
filter: `match_id=eq.${matchId}`,
},
(payload) => {
const updatedMessage = payload.new as Message;
setMessages(prev =>
prev.map(m => m.id === updatedMessage.id ? updatedMessage : m)
);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [matchId, userId, onNewMessage]);
return {
messages,
setMessages,
loading,
isFocusedRef,
};
};