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
This commit is contained in:
2026-02-09 19:23:45 +01:00
parent aabce1a5b0
commit 3ad4a77951
2 changed files with 13 additions and 7 deletions

View File

@@ -634,7 +634,7 @@ NODE_ENV=development # development = pretty logs, production = JSON
- Tracks conversationId for message continuity across sessions - Tracks conversationId for message continuity across sessions
- ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper - ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height) - 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" - keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete(mode, occurrenceDate) - fully implemented with recurring delete modes - `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 - `ChatService`: sendMessage(), confirmEvent(deleteMode, occurrenceDate), rejectEvent(), getConversations(), getConversation(), updateProposalEvent() - fully implemented with cursor pagination, recurring delete support, and proposal editing

View File

@@ -64,11 +64,11 @@ const Chat = () => {
string | undefined string | undefined
>(); >();
const [hasLoadedMessages, setHasLoadedMessages] = useState(false); const [hasLoadedMessages, setHasLoadedMessages] = useState(false);
const needsInitialScroll = useRef(false);
useEffect(() => { useEffect(() => {
const keyboardDidShow = Keyboard.addListener( const keyboardDidShow = Keyboard.addListener("keyboardDidShow", () =>
"keyboardDidShow", scrollToEnd(),
scrollToEnd,
); );
return () => keyboardDidShow.remove(); return () => keyboardDidShow.remove();
}, []); }, []);
@@ -90,7 +90,7 @@ const Chat = () => {
await ChatService.getConversation(conversationId); await ChatService.getConversation(conversationId);
const clientMessages = serverMessages.map(chatMessageToMessageData); const clientMessages = serverMessages.map(chatMessageToMessageData);
addMessages(clientMessages); addMessages(clientMessages);
scrollToEnd(); needsInitialScroll.current = true;
} }
} catch (error) { } catch (error) {
console.error("Failed to load messages:", error); console.error("Failed to load messages:", error);
@@ -102,9 +102,9 @@ const Chat = () => {
}, [isAuthLoading, isAuthenticated, hasLoadedMessages]), }, [isAuthLoading, isAuthenticated, hasLoadedMessages]),
); );
const scrollToEnd = () => { const scrollToEnd = (animated = true) => {
setTimeout(() => { setTimeout(() => {
listRef.current?.scrollToEnd({ animated: true }); listRef.current?.scrollToEnd({ animated });
}, 100); }, 100);
}; };
@@ -277,6 +277,12 @@ const Chat = () => {
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
keyboardDismissMode="interactive" keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
onContentSizeChange={() => {
if (needsInitialScroll.current) {
needsInitialScroll.current = false;
listRef.current?.scrollToEnd({ animated: false });
}
}}
ListFooterComponent={ ListFooterComponent={
isWaitingForResponse ? <TypingIndicator /> : null isWaitingForResponse ? <TypingIndicator /> : null
} }