diff --git a/android/src/screens/ChatScreen.tsx b/android/src/screens/ChatScreen.tsx index 9a7b3cf..8533e94 100644 --- a/android/src/screens/ChatScreen.tsx +++ b/android/src/screens/ChatScreen.tsx @@ -1041,11 +1041,20 @@ const ChatScreen: React.FC = () => { for (const cmid of Array.from(ackTimers.current.keys())) { clearAckTimer(cmid); } - setMessages(prev => prev.map(m => - m.sender === 'user' && m.deliveryStatus === 'sending' - ? { ...m, deliveryStatus: 'sent' } - : m - )); + // Reference-stable: wenn keine Bubble zu aendern ist, geben wir + // prev unveraendert zurueck. Sonst triggert .map() ein neues + // Array + Re-Render, was waehrend einer aktiven Such-Scroll- + // Sequenz die FlatList-Layouts invalidiert → permanenter + // onScrollToIndexFailed-Loop. + setMessages(prev => { + const needs = prev.some(m => m.sender === 'user' && m.deliveryStatus === 'sending'); + if (!needs) return prev; + return prev.map(m => + m.sender === 'user' && m.deliveryStatus === 'sending' + ? { ...m, deliveryStatus: 'sent' } + : m, + ); + }); } // In den Gedanken-Stream einfuegen. Dedup gegen identische Folge- // Events (z.B. zwei mal 'thinking' direkt hintereinander). Tool- @@ -1394,11 +1403,18 @@ const ChatScreen: React.FC = () => { // ein neuer Search-Hit kommt, damit alte Retries nicht den neuen // Scroll-Versuch durcheinanderbringen ("permanent springen"-Bug). const pendingScrollRetry = useRef | null>(null); + // Zaehler fuer fehlgeschlagene Scroll-Retries. Hartes Limit gegen + // Endlos-Loops wenn das Item-Layout aus irgendwelchen Gruenden nie + // verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die + // FlatList re-rendert). + const scrollRetryCount = useRef(0); + const MAX_SCROLL_RETRIES = 3; const clearPendingScrollRetry = () => { if (pendingScrollRetry.current) { clearTimeout(pendingScrollRetry.current); pendingScrollRetry.current = null; } + scrollRetryCount.current = 0; }; // Bei Search-Index-Wechsel zur entsprechenden Bubble scrollen. @@ -2152,13 +2168,24 @@ const ChatScreen: React.FC = () => { scrollEventThrottle={120} onScrollToIndexFailed={(info) => { // FlatList kennt das Item-Layout noch nicht. Wir scrollen grob in - // die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen EINMAL - // nach 300ms praezise nachzusetzen. Mehr Retries → Endlos-Cascade - // (jeder failed Retry triggert wieder den Handler → 3, 9, 27 ... - // Scrolls in der Pipeline = der "permanent springen"-Bug). + // die Naehe (Average-Item-Hoehe-Schaetzung) und versuchen bis zu + // MAX_SCROLL_RETRIES mal praezise nachzusetzen. Danach geben wir + // auf — User sieht die Bubble in der ungefaehren Naehe und kann + // selber finetunen. Frueher: jeder failed Retry triggerte einen + // neuen Retry ohne Limit → "permanent springen"-Bug, vor allem + // wenn waehrenddessen setMessages die Layouts invalidierte. const offset = info.averageItemLength * info.index; try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {} - clearPendingScrollRetry(); + if (pendingScrollRetry.current) { + clearTimeout(pendingScrollRetry.current); + pendingScrollRetry.current = null; + } + if (scrollRetryCount.current >= MAX_SCROLL_RETRIES) { + // Aufgeben — Item ist offenbar nicht stabil renderbar + scrollRetryCount.current = 0; + return; + } + scrollRetryCount.current += 1; pendingScrollRetry.current = setTimeout(() => { pendingScrollRetry.current = null; try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}