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