From 3ad4a77951cdf844ffb48809c63f84bc083937e2 Mon Sep 17 00:00:00 2001 From: Linus Waldowsky Date: Mon, 9 Feb 2026 19:23:45 +0100 Subject: [PATCH] fix: chat starts scrolled to bottom instead of visibly scrolling down - Use onContentSizeChange to scroll after FlashList renders content - Scroll without animation on initial load via needsInitialScroll ref - Remove unreliable 100ms timeout scrollToEnd from message loading --- CLAUDE.md | 2 +- apps/client/src/app/(tabs)/chat.tsx | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8b44818..9d57e79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -634,7 +634,7 @@ NODE_ENV=development # development = pretty logs, production = JSON - Tracks conversationId for message continuity across sessions - ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper - KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height) - - Auto-scroll to end on new messages and keyboard show + - Auto-scroll to end on new messages and keyboard show; initial load uses `onContentSizeChange` with `animated: false` to start at bottom without visible scrolling - keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled" - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes - `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editing diff --git a/apps/client/src/app/(tabs)/chat.tsx b/apps/client/src/app/(tabs)/chat.tsx index ea3b077..b712911 100644 --- a/apps/client/src/app/(tabs)/chat.tsx +++ b/apps/client/src/app/(tabs)/chat.tsx @@ -64,11 +64,11 @@ const Chat = () => { string | undefined >(); const [hasLoadedMessages, setHasLoadedMessages] = useState(false); + const needsInitialScroll = useRef(false); useEffect(() => { - const keyboardDidShow = Keyboard.addListener( - "keyboardDidShow", - scrollToEnd, + const keyboardDidShow = Keyboard.addListener("keyboardDidShow", () => + scrollToEnd(), ); return () => keyboardDidShow.remove(); }, []); @@ -90,7 +90,7 @@ const Chat = () => { await ChatService.getConversation(conversationId); const clientMessages = serverMessages.map(chatMessageToMessageData); addMessages(clientMessages); - scrollToEnd(); + needsInitialScroll.current = true; } } catch (error) { console.error("Failed to load messages:", error); @@ -102,9 +102,9 @@ const Chat = () => { }, [isAuthLoading, isAuthenticated, hasLoadedMessages]), ); - const scrollToEnd = () => { + const scrollToEnd = (animated = true) => { setTimeout(() => { - listRef.current?.scrollToEnd({ animated: true }); + listRef.current?.scrollToEnd({ animated }); }, 100); }; @@ -277,6 +277,12 @@ const Chat = () => { keyExtractor={(item) => item.id} keyboardDismissMode="interactive" keyboardShouldPersistTaps="handled" + onContentSizeChange={() => { + if (needsInitialScroll.current) { + needsInitialScroll.current = false; + listRef.current?.scrollToEnd({ animated: false }); + } + }} ListFooterComponent={ isWaitingForResponse ? : null }