mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
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:
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,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user