Matthias Nott
2026-03-07 8cdf33e27c633ac30e8851c4617f6063c141660d
components/chat/MessageList.tsx
....@@ -1,5 +1,5 @@
1
-import React, { useCallback, useEffect, useRef } from "react";
2
-import { FlatList, View } from "react-native";
1
+import React, { useCallback, useMemo } from "react";
2
+import { ActivityIndicator, FlatList, View } from "react-native";
33 import { Message } from "../../types";
44 import { MessageBubble } from "./MessageBubble";
55 import { TypingIndicator } from "./TypingIndicator";
....@@ -9,62 +9,32 @@
99 messages: Message[];
1010 isTyping?: boolean;
1111 onDeleteMessage?: (id: string) => void;
12
+ onLoadMore?: () => void;
13
+ hasMore?: boolean;
1214 }
1315
14
-export function MessageList({ messages, isTyping, onDeleteMessage }: MessageListProps) {
15
- const listRef = useRef<FlatList<Message>>(null);
16
- const prevLengthRef = useRef(0);
17
-
18
- // Track the last message's content so transcript reflections trigger a scroll
19
- const lastContent = messages.length > 0 ? messages[messages.length - 1].content : "";
20
-
21
- // Flag: when true, every content size change triggers a scroll to bottom.
22
- // Used for bulk loads (restart, session switch) where FlatList renders lazily.
23
- const bulkScrollRef = useRef(false);
24
-
25
- useEffect(() => {
26
- if (messages.length > 0) {
27
- const delta = Math.abs(messages.length - prevLengthRef.current);
28
- if (delta > 1) {
29
- // Bulk load — let onContentSizeChange handle scrolling
30
- bulkScrollRef.current = true;
31
- setTimeout(() => { bulkScrollRef.current = false; }, 3000);
32
- } else {
33
- // Single new message — smooth scroll
34
- setTimeout(() => {
35
- listRef.current?.scrollToEnd({ animated: true });
36
- }, 50);
37
- }
38
- }
39
- prevLengthRef.current = messages.length;
40
- }, [messages.length, isTyping, lastContent]);
41
-
42
- const handleContentSizeChange = useCallback(() => {
43
- if (bulkScrollRef.current) {
44
- listRef.current?.scrollToEnd({ animated: false });
45
- }
46
- }, []);
16
+export function MessageList({ messages, isTyping, onDeleteMessage, onLoadMore, hasMore }: MessageListProps) {
17
+ // Inverted FlatList renders bottom-up — newest messages at the bottom (visually),
18
+ // which means we reverse the data so index 0 = newest = rendered at bottom.
19
+ const invertedData = useMemo(() => [...messages].reverse(), [messages]);
4720
4821 // Play from a voice message and auto-chain all consecutive assistant voice messages after it
4922 const handlePlayVoice = useCallback(async (messageId: string) => {
5023 const idx = messages.findIndex((m) => m.id === messageId);
5124 if (idx === -1) return;
5225
53
- // Collect this message + all consecutive assistant voice messages after it
5426 const chain: Message[] = [];
5527 for (let i = idx; i < messages.length; i++) {
5628 const m = messages[i];
5729 if (m.role === "assistant" && m.type === "voice" && m.audioUri) {
5830 chain.push(m);
5931 } else if (i > idx) {
60
- // Stop at the first non-voice or non-assistant message
6132 break;
6233 }
6334 }
6435
6536 if (chain.length === 0) return;
6637
67
- // Stop current playback, then queue all chunks
6838 await stopPlayback();
6939 for (const m of chain) {
7040 playAudio(m.audioUri!);
....@@ -73,8 +43,8 @@
7343
7444 return (
7545 <FlatList
76
- ref={listRef}
77
- data={messages}
46
+ inverted
47
+ data={invertedData}
7848 keyExtractor={(item) => item.id}
7949 renderItem={({ item }) => (
8050 <MessageBubble
....@@ -83,15 +53,23 @@
8353 onPlayVoice={handlePlayVoice}
8454 />
8555 )}
86
- onContentSizeChange={handleContentSizeChange}
56
+ onEndReached={hasMore ? onLoadMore : undefined}
57
+ onEndReachedThreshold={0.5}
8758 contentContainerStyle={{ paddingVertical: 12 }}
8859 showsVerticalScrollIndicator={false}
89
- ListFooterComponent={
60
+ ListHeaderComponent={
9061 <>
9162 {isTyping && <TypingIndicator />}
9263 <View style={{ height: 4 }} />
9364 </>
9465 }
66
+ ListFooterComponent={
67
+ hasMore ? (
68
+ <View style={{ paddingVertical: 16, alignItems: "center" }}>
69
+ <ActivityIndicator size="small" />
70
+ </View>
71
+ ) : null
72
+ }
9573 />
9674 );
9775 }