Matthias Nott
2026-03-07 8cdf33e27c633ac30e8851c4617f6063c141660d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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";
import { stopPlayback, playAudio } from "../../services/audio";
interface MessageListProps {
  messages: Message[];
  isTyping?: boolean;
  onDeleteMessage?: (id: string) => void;
  onLoadMore?: () => void;
  hasMore?: boolean;
}
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;
    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) {
        break;
      }
    }
    if (chain.length === 0) return;
    await stopPlayback();
    for (const m of chain) {
      playAudio(m.audioUri!);
    }
  }, [messages]);
  return (
    <FlatList
      inverted
      data={invertedData}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <MessageBubble
          message={item}
          onDelete={onDeleteMessage}
          onPlayVoice={handlePlayVoice}
        />
      )}
      onEndReached={hasMore ? onLoadMore : undefined}
      onEndReachedThreshold={0.5}
      contentContainerStyle={{ paddingVertical: 12 }}
      showsVerticalScrollIndicator={false}
      ListHeaderComponent={
        <>
          {isTyping && <TypingIndicator />}
          <View style={{ height: 4 }} />
        </>
      }
      ListFooterComponent={
        hasMore ? (
          <View style={{ paddingVertical: 16, alignItems: "center" }}>
            <ActivityIndicator size="small" />
          </View>
        ) : null
      }
    />
  );
}