From 860334399ebb220c85721f0d62b3e8c0c19725dd Mon Sep 17 00:00:00 2001 From: Ayaanshaikh12243 Date: Mon, 26 Jan 2026 19:18:23 +0530 Subject: [PATCH 1/2] ISSUE-174 --- src/hooks/useMessaging.ts | 185 +++++++++++++++++++++++++------------- 1 file changed, 122 insertions(+), 63 deletions(-) diff --git a/src/hooks/useMessaging.ts b/src/hooks/useMessaging.ts index 8602956a..a5541827 100644 --- a/src/hooks/useMessaging.ts +++ b/src/hooks/useMessaging.ts @@ -158,6 +158,10 @@ export const useSendMessage = () => { }); }; +// Connection pool for message channels +const messageChannels = new Map(); +const channelSubscribers = new Map void>>(); + // Hook for real-time message updates export const useRealtimeMessages = (conversationId: number) => { const queryClient = useQueryClient(); @@ -166,41 +170,73 @@ export const useRealtimeMessages = (conversationId: number) => { useEffect(() => { if (!user || !conversationId) return; - const channel = supabase - .channel(`messages:${conversationId}`) - .on( - 'postgres_changes', - { - event: 'INSERT', - schema: 'public', - table: 'Messages', - filter: `conversation_id=eq.${conversationId}`, - }, - () => { - queryClient.invalidateQueries({ queryKey: ['messages', conversationId] }); - queryClient.invalidateQueries({ queryKey: ['conversations'] }); - } - ) - .on( - 'postgres_changes', - { - event: 'UPDATE', - schema: 'public', - table: 'Messages', - filter: `conversation_id=eq.${conversationId}`, - }, - () => { - queryClient.invalidateQueries({ queryKey: ['messages', conversationId] }); - } - ) - .subscribe(); + const invalidateQueries = () => { + queryClient.invalidateQueries({ queryKey: ['messages', conversationId] }); + queryClient.invalidateQueries({ queryKey: ['conversations'] }); + }; + + // Use connection pooling + if (!messageChannels.has(conversationId)) { + const channel = supabase + .channel(`messages:${conversationId}`) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'Messages', + filter: `conversation_id=eq.${conversationId}`, + }, + () => { + // Notify all subscribers for this conversation + const subscribers = channelSubscribers.get(conversationId); + subscribers?.forEach(callback => callback()); + } + ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'Messages', + filter: `conversation_id=eq.${conversationId}`, + }, + () => { + const subscribers = channelSubscribers.get(conversationId); + subscribers?.forEach(callback => callback()); + } + ) + .subscribe(); + + messageChannels.set(conversationId, channel); + channelSubscribers.set(conversationId, new Set()); + } + + // Add this component as subscriber + channelSubscribers.get(conversationId)?.add(invalidateQueries); return () => { - supabase.removeChannel(channel); + // Remove subscriber + const subscribers = channelSubscribers.get(conversationId); + subscribers?.delete(invalidateQueries); + + // Clean up channel if no subscribers + if (subscribers?.size === 0) { + const channel = messageChannels.get(conversationId); + if (channel) { + supabase.removeChannel(channel); + messageChannels.delete(conversationId); + channelSubscribers.delete(conversationId); + } + } }; }, [conversationId, user, queryClient]); }; +// Shared presence channel for connection pooling +let sharedPresenceChannel: any = null; +let presenceSubscribers = new Set<(users: UserPresence[]) => void>(); + // Hook for user presence export const useUserPresence = () => { const { user } = useAuth(); @@ -211,56 +247,79 @@ export const useUserPresence = () => { // Update user status to online const updatePresence = async () => { - await supabase - .from('UserPresence') - .upsert({ - user_id: user.id, - status: 'online', - last_seen: new Date().toISOString(), - }); + try { + await supabase + .from('UserPresence') + .upsert({ + user_id: user.id, + status: 'online', + last_seen: new Date().toISOString(), + }); + } catch (error) { + console.warn('Failed to update presence:', error); + } }; updatePresence(); - // Set up real-time subscription for presence updates - const channel = supabase - .channel('user-presence') - .on( - 'postgres_changes', - { - event: '*', - schema: 'public', - table: 'UserPresence', - }, - () => { - // Refetch presence data - fetchPresence(); - } - ) - .subscribe(); + // Use shared channel for connection pooling + if (!sharedPresenceChannel) { + sharedPresenceChannel = supabase + .channel('shared-user-presence') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'UserPresence', + }, + () => { + fetchPresence(); + } + ) + .subscribe(); + } const fetchPresence = async () => { - const { data } = await supabase - .from('UserPresence') - .select('*') - .eq('status', 'online'); - - if (data) setOnlineUsers(data); + try { + const { data } = await supabase + .from('UserPresence') + .select('*') + .eq('status', 'online'); + + if (data) { + // Notify all subscribers + presenceSubscribers.forEach(callback => callback(data)); + } + } catch (error) { + console.warn('Failed to fetch presence:', error); + } }; + // Add this component as subscriber + presenceSubscribers.add(setOnlineUsers); fetchPresence(); - // Update presence every 30 seconds - const interval = setInterval(updatePresence, 30000); + // Reduced frequency: Update presence every 2 minutes + const interval = setInterval(updatePresence, 120000); - // Set status to offline on unmount + // Cleanup on unmount return () => { clearInterval(interval); - supabase.removeChannel(channel); + presenceSubscribers.delete(setOnlineUsers); + + // Clean up shared channel if no subscribers + if (presenceSubscribers.size === 0 && sharedPresenceChannel) { + supabase.removeChannel(sharedPresenceChannel); + sharedPresenceChannel = null; + } + + // Set status to offline supabase .from('UserPresence') .update({ status: 'offline', last_seen: new Date().toISOString() }) - .eq('user_id', user.id); + .eq('user_id', user.id) + .then(() => {}); }; }, [user]); From 838404ed8386ff139224d049225eb5cb71aeb268 Mon Sep 17 00:00:00 2001 From: Ayaanshaikh12243 Date: Mon, 26 Jan 2026 19:22:21 +0530 Subject: [PATCH 2/2] ISSUE-173 --- src/context/AuthContext.tsx | 128 ++++++++++++++++++++++++++---------- src/utils/errorHandler.ts | 54 +++++++++++++++ 2 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 src/utils/errorHandler.ts diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index c8c664d6..36aadf51 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -56,28 +56,46 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const signInWithGithub = async () => { if (!isBackendAvailable || !supabase) { - throw new Error("Authentication is disabled in demo mode"); + throw new Error("GitHub authentication is disabled in demo mode"); } - const { error } = await supabase.auth.signInWithOAuth({ - provider: "github", - options: { - redirectTo: window.location.origin, - }, - }); - if (error) throw error; + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: "github", + options: { + redirectTo: window.location.origin, + }, + }); + if (error) throw error; + } catch (networkError: any) { + throw new Error(`GitHub login failed: ${networkError.message || 'Connection failed'}`); + } }; const signInWithEmail = async (email: string, password: string) => { if (!isBackendAvailable || !supabase) { - return { error: null }; + return { + error: { + message: "Authentication is disabled in demo mode", + name: "DemoModeError" + } as AuthError + }; } - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - return { error }; + try { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + return { error }; + } catch (networkError: any) { + return { + error: { + message: `Network error: ${networkError.message || 'Connection failed'}`, + name: "NetworkError" + } as AuthError + }; + } }; const signUpWithEmail = async ( @@ -86,18 +104,32 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { metadata?: { full_name?: string } ) => { if (!isBackendAvailable || !supabase) { - return { error: null }; + return { + error: { + message: "Registration is disabled in demo mode", + name: "DemoModeError" + } as AuthError + }; } - const { error } = await supabase.auth.signUp({ - email, - password, - options: { - data: metadata, - emailRedirectTo: `${window.location.origin}/`, - }, - }); - return { error }; + try { + const { error } = await supabase.auth.signUp({ + email, + password, + options: { + data: metadata, + emailRedirectTo: `${window.location.origin}/`, + }, + }); + return { error }; + } catch (networkError: any) { + return { + error: { + message: `Registration failed: ${networkError.message || 'Connection failed'}`, + name: "NetworkError" + } as AuthError + }; + } }; const signOut = async () => { @@ -109,24 +141,52 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const resetPassword = async (email: string) => { if (!isBackendAvailable || !supabase) { - return { error: null }; + return { + error: { + message: "Password reset is disabled in demo mode", + name: "DemoModeError" + } as AuthError + }; } - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${window.location.origin}/reset-password`, - }); - return { error }; + try { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password`, + }); + return { error }; + } catch (networkError: any) { + return { + error: { + message: `Password reset failed: ${networkError.message || 'Connection failed'}`, + name: "NetworkError" + } as AuthError + }; + } }; const updatePassword = async (password: string) => { if (!isBackendAvailable || !supabase) { - return { error: null }; + return { + error: { + message: "Password update is disabled in demo mode", + name: "DemoModeError" + } as AuthError + }; } - const { error } = await supabase.auth.updateUser({ - password, - }); - return { error }; + try { + const { error } = await supabase.auth.updateUser({ + password, + }); + return { error }; + } catch (networkError: any) { + return { + error: { + message: `Password update failed: ${networkError.message || 'Connection failed'}`, + name: "NetworkError" + } as AuthError + }; + } }; const updateProfile = async (metadata: { full_name?: string; avatar_url?: string; bio?: string; location?: string; website?: string; github?: string; twitter?: string }) => { diff --git a/src/utils/errorHandler.ts b/src/utils/errorHandler.ts new file mode 100644 index 00000000..0d37f705 --- /dev/null +++ b/src/utils/errorHandler.ts @@ -0,0 +1,54 @@ +import { AuthError } from '@supabase/supabase-js'; + +export interface AppError { + message: string; + type: 'network' | 'auth' | 'validation' | 'demo'; + code?: string; +} + +export const handleAuthError = (error: AuthError | null): AppError | null => { + if (!error) return null; + + // Map common Supabase auth errors to user-friendly messages + const errorMap: Record = { + 'invalid_credentials': 'Invalid email or password', + 'email_not_confirmed': 'Please check your email and confirm your account', + 'signup_disabled': 'New registrations are currently disabled', + 'email_address_invalid': 'Please enter a valid email address', + 'password_too_short': 'Password must be at least 6 characters', + 'user_not_found': 'No account found with this email', + 'email_address_not_authorized': 'This email is not authorized to sign up', + }; + + return { + message: errorMap[error.message] || error.message || 'Authentication failed', + type: 'auth', + code: error.message + }; +}; + +export const handleNetworkError = (error: any): AppError => { + if (error.name === 'NetworkError' || error.code === 'NETWORK_ERROR') { + return { + message: 'Network connection failed. Please check your internet connection.', + type: 'network' + }; + } + + if (error.code === 'TIMEOUT') { + return { + message: 'Request timed out. Please try again.', + type: 'network' + }; + } + + return { + message: error.message || 'An unexpected error occurred', + type: 'network' + }; +}; + +export const isDemoModeError = (error: any): boolean => { + return error?.name === 'DemoModeError' || + error?.message?.includes('demo mode'); +}; \ No newline at end of file