From 45499edc9a0b78094601f577c707da2374b1765d Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sun, 25 Jan 2026 20:29:11 +0000 Subject: [PATCH] Fix chat message race condition on notification tap --- .../src/components/discovery/ContentTile.tsx | 12 --------- .../src/components/questions/QuestionCard.tsx | 2 +- .../src/features/profile/ProfileScreen.tsx | 2 +- .../profile/screens/ProfileSettingsScreen.tsx | 3 ++- .../src/hooks/useMessageSubscription.ts | 25 +++++++++++++++++-- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/apps/mobile/src/components/discovery/ContentTile.tsx b/apps/mobile/src/components/discovery/ContentTile.tsx index 7e511f6..4c7936a 100644 --- a/apps/mobile/src/components/discovery/ContentTile.tsx +++ b/apps/mobile/src/components/discovery/ContentTile.tsx @@ -82,11 +82,6 @@ export function ContentTile({ pack, isLocked, isNew = false, progress, onPress } {/* Content */} - {pack.category?.name && ( - - {pack.category.name} - - )} {pack.name} @@ -173,13 +168,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginTop: spacing.sm, }, - categoryText: { - ...typography.caption2, - color: 'rgba(255, 255, 255, 0.7)', - textTransform: 'uppercase', - letterSpacing: 1, - marginBottom: 2, - }, titleText: { ...typography.headline, color: colors.text, diff --git a/apps/mobile/src/components/questions/QuestionCard.tsx b/apps/mobile/src/components/questions/QuestionCard.tsx index 78cbc54..5ffa9b5 100644 --- a/apps/mobile/src/components/questions/QuestionCard.tsx +++ b/apps/mobile/src/components/questions/QuestionCard.tsx @@ -20,7 +20,7 @@ import { SharePreviewModal } from "../share"; const ACTION_BUTTON_SIZE = 72; const ACTION_BUTTON_OVERHANG_PERCENT = 0.5; // 50% of button hangs below the card const actionButtonOffset = ACTION_BUTTON_SIZE * ACTION_BUTTON_OVERHANG_PERCENT; -const actionButtonBackground = colors.muted; +const actionButtonBackground = '#3a3a42'; // Haptics helper - not supported on web const triggerHaptic = async (style: 'light' | 'medium' | 'heavy' = 'medium') => { diff --git a/apps/mobile/src/features/profile/ProfileScreen.tsx b/apps/mobile/src/features/profile/ProfileScreen.tsx index ebd1c23..3ad37c3 100644 --- a/apps/mobile/src/features/profile/ProfileScreen.tsx +++ b/apps/mobile/src/features/profile/ProfileScreen.tsx @@ -121,7 +121,7 @@ export function ProfileScreen() { {user?.name || "User"} - + {isAnonymous ? "Unsaved account" : (user?.email || "")} diff --git a/apps/mobile/src/features/profile/screens/ProfileSettingsScreen.tsx b/apps/mobile/src/features/profile/screens/ProfileSettingsScreen.tsx index 618ece5..8de63f6 100644 --- a/apps/mobile/src/features/profile/screens/ProfileSettingsScreen.tsx +++ b/apps/mobile/src/features/profile/screens/ProfileSettingsScreen.tsx @@ -260,7 +260,7 @@ export function ProfileSettingsScreen() { color={colors.textTertiary} style={styles.inputIcon} /> - + {authEmail || (isAnonymous ? 'Guest account' : 'Loading...')} @@ -407,6 +407,7 @@ const styles = StyleSheet.create({ readOnlyText: { ...typography.body, color: colors.textSecondary, + flex: 1, }, // Gender genderDescription: { diff --git a/apps/mobile/src/hooks/useMessageSubscription.ts b/apps/mobile/src/hooks/useMessageSubscription.ts index 5f67ade..5a3e4c0 100644 --- a/apps/mobile/src/hooks/useMessageSubscription.ts +++ b/apps/mobile/src/hooks/useMessageSubscription.ts @@ -41,6 +41,10 @@ export const useMessageSubscription = ( const [loading, setLoading] = useState(true); const isFocusedRef = useRef(false); + // Track messages that arrive via real-time during initial fetch to avoid race condition + const pendingRealtimeMessagesRef = useRef([]); + const isFetchingRef = useRef(false); + // Filter out messages that the user has deleted for themselves const messages = useMemo(() => { return allMessages.filter(m => !deletedMessageIds.has(m.id)); @@ -104,6 +108,8 @@ export const useMessageSubscription = ( const fetchMessagesAndMarkRead = async () => { setLoading(true); + isFetchingRef.current = true; + pendingRealtimeMessagesRef.current = []; setAllMessages([]); // Clear previous chat's messages immediately const { data } = await supabase @@ -113,7 +119,14 @@ export const useMessageSubscription = ( .order('created_at', { ascending: false }); if (data) { - setAllMessages(data); + // Merge any messages that arrived via real-time during the fetch + const pendingMessages = pendingRealtimeMessagesRef.current; + const fetchedIds = new Set(data.map(m => m.id)); + const newRealtimeMessages = pendingMessages.filter(m => !fetchedIds.has(m.id)); + + // Combine: new real-time messages first (newest), then fetched data + const mergedMessages = [...newRealtimeMessages, ...data]; + setAllMessages(mergedMessages); const now = new Date().toISOString(); @@ -145,6 +158,8 @@ export const useMessageSubscription = ( } } + isFetchingRef.current = false; + pendingRealtimeMessagesRef.current = []; setLoading(false); }; @@ -179,7 +194,13 @@ export const useMessageSubscription = ( newMessage.read_at = now; } - setAllMessages(prev => [newMessage, ...prev]); + // If initial fetch is in progress, queue the message to avoid race condition + // where setAllMessages(data) would overwrite this message + if (isFetchingRef.current) { + pendingRealtimeMessagesRef.current.push(newMessage); + } else { + setAllMessages(prev => [newMessage, ...prev]); + } onNewMessage?.(); } )