Matthias Nott
2026-03-07 c23dfe16e95713e7058137308bdbc28419609a39
components/chat/MessageList.tsx
....@@ -1,36 +1,76 @@
1
-import React, { useEffect, useRef } from "react";
1
+import React, { useCallback, useEffect, useRef } from "react";
22 import { FlatList, View } from "react-native";
33 import { Message } from "../../types";
44 import { MessageBubble } from "./MessageBubble";
5
+import { TypingIndicator } from "./TypingIndicator";
6
+import { stopPlayback, playAudio } from "../../services/audio";
57
68 interface MessageListProps {
79 messages: Message[];
10
+ isTyping?: boolean;
11
+ onDeleteMessage?: (id: string) => void;
812 }
913
10
-export function MessageList({ messages }: MessageListProps) {
14
+export function MessageList({ messages, isTyping, onDeleteMessage }: MessageListProps) {
1115 const listRef = useRef<FlatList<Message>>(null);
1216
1317 useEffect(() => {
1418 if (messages.length > 0) {
15
- // Small delay to allow layout to complete
1619 setTimeout(() => {
1720 listRef.current?.scrollToEnd({ animated: true });
1821 }, 50);
1922 }
20
- }, [messages.length]);
23
+ }, [messages.length, isTyping]);
24
+
25
+ // Play from a voice message and auto-chain all consecutive assistant voice messages after it
26
+ const handlePlayVoice = useCallback(async (messageId: string) => {
27
+ const idx = messages.findIndex((m) => m.id === messageId);
28
+ if (idx === -1) return;
29
+
30
+ // Collect this message + all consecutive assistant voice messages after it
31
+ const chain: Message[] = [];
32
+ for (let i = idx; i < messages.length; i++) {
33
+ const m = messages[i];
34
+ if (m.role === "assistant" && m.type === "voice" && m.audioUri) {
35
+ chain.push(m);
36
+ } else if (i > idx) {
37
+ // Stop at the first non-voice or non-assistant message
38
+ break;
39
+ }
40
+ }
41
+
42
+ if (chain.length === 0) return;
43
+
44
+ // Stop current playback, then queue all chunks
45
+ await stopPlayback();
46
+ for (const m of chain) {
47
+ playAudio(m.audioUri!);
48
+ }
49
+ }, [messages]);
2150
2251 return (
2352 <FlatList
2453 ref={listRef}
2554 data={messages}
2655 keyExtractor={(item) => item.id}
27
- renderItem={({ item }) => <MessageBubble message={item} />}
56
+ renderItem={({ item }) => (
57
+ <MessageBubble
58
+ message={item}
59
+ onDelete={onDeleteMessage}
60
+ onPlayVoice={handlePlayVoice}
61
+ />
62
+ )}
2863 contentContainerStyle={{ paddingVertical: 12 }}
2964 onContentSizeChange={() => {
3065 listRef.current?.scrollToEnd({ animated: false });
3166 }}
3267 showsVerticalScrollIndicator={false}
33
- ListFooterComponent={<View style={{ height: 4 }} />}
68
+ ListFooterComponent={
69
+ <>
70
+ {isTyping && <TypingIndicator />}
71
+ <View style={{ height: 4 }} />
72
+ </>
73
+ }
3474 />
3575 );
3676 }