From 8cdf33e27c633ac30e8851c4617f6063c141660d Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Sat, 07 Mar 2026 17:53:05 +0100
Subject: [PATCH] fix: audio routing, WebSocket reconnection, inverted chat list

---
 components/chat/MessageList.tsx |   62 ++++++++++---------------------
 1 files changed, 20 insertions(+), 42 deletions(-)

diff --git a/components/chat/MessageList.tsx b/components/chat/MessageList.tsx
index c3eb13e..b65eb60 100644
--- a/components/chat/MessageList.tsx
+++ b/components/chat/MessageList.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect, useRef } from "react";
-import { FlatList, View } from "react-native";
+import React, { useCallback, useMemo } from "react";
+import { ActivityIndicator, FlatList, View } from "react-native";
 import { Message } from "../../types";
 import { MessageBubble } from "./MessageBubble";
 import { TypingIndicator } from "./TypingIndicator";
@@ -9,62 +9,32 @@
   messages: Message[];
   isTyping?: boolean;
   onDeleteMessage?: (id: string) => void;
+  onLoadMore?: () => void;
+  hasMore?: boolean;
 }
 
-export function MessageList({ messages, isTyping, onDeleteMessage }: MessageListProps) {
-  const listRef = useRef<FlatList<Message>>(null);
-  const prevLengthRef = useRef(0);
-
-  // Track the last message's content so transcript reflections trigger a scroll
-  const lastContent = messages.length > 0 ? messages[messages.length - 1].content : "";
-
-  // Flag: when true, every content size change triggers a scroll to bottom.
-  // Used for bulk loads (restart, session switch) where FlatList renders lazily.
-  const bulkScrollRef = useRef(false);
-
-  useEffect(() => {
-    if (messages.length > 0) {
-      const delta = Math.abs(messages.length - prevLengthRef.current);
-      if (delta > 1) {
-        // Bulk load — let onContentSizeChange handle scrolling
-        bulkScrollRef.current = true;
-        setTimeout(() => { bulkScrollRef.current = false; }, 3000);
-      } else {
-        // Single new message — smooth scroll
-        setTimeout(() => {
-          listRef.current?.scrollToEnd({ animated: true });
-        }, 50);
-      }
-    }
-    prevLengthRef.current = messages.length;
-  }, [messages.length, isTyping, lastContent]);
-
-  const handleContentSizeChange = useCallback(() => {
-    if (bulkScrollRef.current) {
-      listRef.current?.scrollToEnd({ animated: false });
-    }
-  }, []);
+export function MessageList({ messages, isTyping, onDeleteMessage, onLoadMore, hasMore }: MessageListProps) {
+  // Inverted FlatList renders bottom-up — newest messages at the bottom (visually),
+  // which means we reverse the data so index 0 = newest = rendered at bottom.
+  const invertedData = useMemo(() => [...messages].reverse(), [messages]);
 
   // 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!);
@@ -73,8 +43,8 @@
 
   return (
     <FlatList
-      ref={listRef}
-      data={messages}
+      inverted
+      data={invertedData}
       keyExtractor={(item) => item.id}
       renderItem={({ item }) => (
         <MessageBubble
@@ -83,15 +53,23 @@
           onPlayVoice={handlePlayVoice}
         />
       )}
-      onContentSizeChange={handleContentSizeChange}
+      onEndReached={hasMore ? onLoadMore : undefined}
+      onEndReachedThreshold={0.5}
       contentContainerStyle={{ paddingVertical: 12 }}
       showsVerticalScrollIndicator={false}
-      ListFooterComponent={
+      ListHeaderComponent={
         <>
           {isTyping && <TypingIndicator />}
           <View style={{ height: 4 }} />
         </>
       }
+      ListFooterComponent={
+        hasMore ? (
+          <View style={{ paddingVertical: 16, alignItems: "center" }}>
+            <ActivityIndicator size="small" />
+          </View>
+        ) : null
+      }
     />
   );
 }

--
Gitblit v1.3.1