From c23dfe16e95713e7058137308bdbc28419609a39 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 11:54:15 +0100
Subject: [PATCH] feat: typing indicator, message deletion, chain playback, autoplay guard

---
 components/chat/MessageList.tsx |   52 ++++++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/components/chat/MessageList.tsx b/components/chat/MessageList.tsx
index 178c46d..0076e5c 100644
--- a/components/chat/MessageList.tsx
+++ b/components/chat/MessageList.tsx
@@ -1,36 +1,76 @@
-import React, { useEffect, useRef } from "react";
+import React, { useCallback, useEffect, useRef } from "react";
 import { FlatList, View } from "react-native";
 import { Message } from "../../types";
 import { MessageBubble } from "./MessageBubble";
+import { TypingIndicator } from "./TypingIndicator";
+import { stopPlayback, playAudio } from "../../services/audio";
 
 interface MessageListProps {
   messages: Message[];
+  isTyping?: boolean;
+  onDeleteMessage?: (id: string) => void;
 }
 
-export function MessageList({ messages }: MessageListProps) {
+export function MessageList({ messages, isTyping, onDeleteMessage }: MessageListProps) {
   const listRef = useRef<FlatList<Message>>(null);
 
   useEffect(() => {
     if (messages.length > 0) {
-      // Small delay to allow layout to complete
       setTimeout(() => {
         listRef.current?.scrollToEnd({ animated: true });
       }, 50);
     }
-  }, [messages.length]);
+  }, [messages.length, isTyping]);
+
+  // Play from a voice message and auto-chain all consecutive assistant voice messages after it
+  const handlePlayVoice = useCallback(async (messageId: string) => {
+    const idx = messages.findIndex((m) => m.id === messageId);
+    if (idx === -1) return;
+
+    // Collect this message + all consecutive assistant voice messages after it
+    const chain: Message[] = [];
+    for (let i = idx; i < messages.length; i++) {
+      const m = messages[i];
+      if (m.role === "assistant" && m.type === "voice" && m.audioUri) {
+        chain.push(m);
+      } else if (i > idx) {
+        // Stop at the first non-voice or non-assistant message
+        break;
+      }
+    }
+
+    if (chain.length === 0) return;
+
+    // Stop current playback, then queue all chunks
+    await stopPlayback();
+    for (const m of chain) {
+      playAudio(m.audioUri!);
+    }
+  }, [messages]);
 
   return (
     <FlatList
       ref={listRef}
       data={messages}
       keyExtractor={(item) => item.id}
-      renderItem={({ item }) => <MessageBubble message={item} />}
+      renderItem={({ item }) => (
+        <MessageBubble
+          message={item}
+          onDelete={onDeleteMessage}
+          onPlayVoice={handlePlayVoice}
+        />
+      )}
       contentContainerStyle={{ paddingVertical: 12 }}
       onContentSizeChange={() => {
         listRef.current?.scrollToEnd({ animated: false });
       }}
       showsVerticalScrollIndicator={false}
-      ListFooterComponent={<View style={{ height: 4 }} />}
+      ListFooterComponent={
+        <>
+          {isTyping && <TypingIndicator />}
+          <View style={{ height: 4 }} />
+        </>
+      }
     />
   );
 }

--
Gitblit v1.3.1