Matthias Nott
2026-03-07 281834df3070cfbdfc28314ab2a2e84d321ca5df
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
76
77
78
79
80
81
82
83
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, 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 : "";
  useEffect(() => {
    if (messages.length > 0) {
      // If the message count changed by more than 1, it's a session switch or
      // initial load — snap to bottom instantly instead of visibly scrolling.
      const delta = Math.abs(messages.length - prevLengthRef.current);
      const animated = delta === 1;
      const delay = delta > 1 ? 200 : 50;
      setTimeout(() => {
        listRef.current?.scrollToEnd({ animated });
      }, delay);
    }
    prevLengthRef.current = messages.length;
  }, [messages.length, isTyping, lastContent]);
  // 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}
          onDelete={onDeleteMessage}
          onPlayVoice={handlePlayVoice}
        />
      )}
      contentContainerStyle={{ paddingVertical: 12 }}
      showsVerticalScrollIndicator={false}
      ListFooterComponent={
        <>
          {isTyping && <TypingIndicator />}
          <View style={{ height: 4 }} />
        </>
      }
    />
  );
}