fix(chat): Gedanken-Stream scrollt jetzt + Suche praeziser

(1) Gedanken-Stream Modal: vorheriger Fix mit onStartShouldSetResponder
    war falsch — der View wurde komplett zum Responder, die FlatList drin
    bekam null Touch-Events. Jetzt: outer View ohne Touch-Handling, ein
    separates TouchableOpacity-Element oberhalb des Sheets nur fuer den
    Tap-Outside-Close. Sheet-View ist plain View → FlatList scrollt frei.

(2) Such-Sprung praeziser: drei Verbesserungen
    - MAX_SCROLL_RETRIES 3 → 6: bei weiten Spruengen (Bubble #150 von
      Position 0) braucht FlatList mehrere Iterationen bis die Items in
      der Naehe gemessen sind
    - Pre-Scroll-Offset: Fallback fuer unmeasured Items ist jetzt der
      dynamische Mittel der bisher gemessenen Items (statt Pauschal-150).
      Beim Cold-Start sind nur die untersten 10 gemessen, aber deren
      Mittel ist immer noch eine bessere Schaetzung
    - Render-Pause nach Pre-Scroll 200 → 350 ms: bei weiten Spruengen
      braucht FlatList Zeit die Items zu mounten und onLayout zu feuern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 11:11:38 +02:00
parent b6a68b7658
commit ce6f5b551e
+30 -21
View File
@@ -1415,7 +1415,10 @@ const ChatScreen: React.FC = () => {
// verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die
// FlatList re-rendert).
const scrollRetryCount = useRef<number>(0);
const MAX_SCROLL_RETRIES = 3;
// 6 Retries: bei weiten Spruengen (Suche auf Bubble #150 von Position 0)
// kann FlatList mehrere Iterationen brauchen bis die Items in der Naehe
// gemessen sind. Vorher 3 = vorzeitig aufgegeben.
const MAX_SCROLL_RETRIES = 6;
const clearPendingScrollRetry = () => {
if (pendingScrollRetry.current) {
clearTimeout(pendingScrollRetry.current);
@@ -1459,13 +1462,18 @@ const ChatScreen: React.FC = () => {
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items
// und liefert einen voellig falschen Sprung).
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
// onLayout) + Fallback AVG fuer noch nicht gemessene. Bei „cold start"
// ist der Cache leer → AVG fuer alle → grob. Beim zweiten Such-Versuch
// sind die Bubbles in der Naehe gemessen → genauer.
// onLayout) + dynamischer Fallback aus dem Mittel der bisher
// gemessenen Items. Beim Cold-Start gibt's nur 10 Messungen (die
// neuesten unten in der invertierten Liste) — der Mittel daraus ist
// immer noch besser als die Pauschal-150.
const measured = Array.from(itemHeights.current.values());
const dynamicAvg = measured.length >= 5
? measured.reduce((a, b) => a + b, 0) / measured.length
: AVG_BUBBLE_HEIGHT;
let preOffset = 0;
const inv = invertedMessagesRef.current;
for (let i = 0; i < idx; i++) {
preOffset += itemHeights.current.get(inv[i].id) || AVG_BUBBLE_HEIGHT;
preOffset += itemHeights.current.get(inv[i].id) || dynamicAvg;
}
try {
flatListRef.current?.scrollToOffset({
@@ -1473,9 +1481,10 @@ const ChatScreen: React.FC = () => {
animated: false,
});
} catch {}
// Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms —
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das
// war Stefans „erst beim zweiten Versuch klappt's"-Bug.
// Nach Render-Pause praezise nachsetzen. 350 ms — bei weiten Spruengen
// (Pre-Scroll 5000+ px) braucht FlatList Zeit die Items dort zu
// mounten und onLayout zu feuern. Zu kurz → averageItemLength im
// Failed-Handler basiert noch auf den falschen Items.
requestAnimationFrame(() => {
setTimeout(() => {
try {
@@ -1483,7 +1492,7 @@ const ChatScreen: React.FC = () => {
} catch {
// onScrollToIndexFailed-Handler uebernimmt den Fallback
}
}, 200);
}, 350);
});
}, [searchIndex, searchMatchIds]);
@@ -2411,19 +2420,19 @@ const ChatScreen: React.FC = () => {
transparent
onRequestClose={() => setThoughtsVisible(false)}
>
<TouchableOpacity
style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}
activeOpacity={1}
onPress={() => setThoughtsVisible(false)}
>
{/* View statt TouchableOpacity, sonst konsumiert das die Touch-
Events und die FlatList laesst sich nicht scrollen.
onStartShouldSetResponder={true} blockt aber die Propagation
an das aeussere TouchableOpacity (close-on-tap-outside). */}
<View style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}>
{/* Tap-Outside-Bereich oberhalb des Sheets — separater Touchable
damit das Sheet-View NICHT als Responder den FlatList-Scroll
blockiert. Frueher hatten wir den ganzen Hintergrund als
TouchableOpacity + inneren View mit onStartShouldSetResponder
= das hat alle Touch-Events kassiert. */}
<TouchableOpacity
style={{flex:1}}
activeOpacity={1}
onPress={() => setThoughtsVisible(false)}
/>
<View
style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}
onStartShouldSetResponder={() => true}
onResponderTerminationRequest={() => false}
>
{/* Drag-Indicator */}
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
@@ -2504,7 +2513,7 @@ const ChatScreen: React.FC = () => {
/>
)}
</View>
</TouchableOpacity>
</View>
</Modal>
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).