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 // verfuegbar wird (z.B. weil setMessages mitten in der Sequenz die
// FlatList re-rendert). // FlatList re-rendert).
const scrollRetryCount = useRef<number>(0); 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 = () => { const clearPendingScrollRetry = () => {
if (pendingScrollRetry.current) { if (pendingScrollRetry.current) {
clearTimeout(pendingScrollRetry.current); clearTimeout(pendingScrollRetry.current);
@@ -1459,13 +1462,18 @@ const ChatScreen: React.FC = () => {
// averageItemLength im Failed-Handler nur auf den ersten ~10 Items // averageItemLength im Failed-Handler nur auf den ersten ~10 Items
// und liefert einen voellig falschen Sprung). // und liefert einen voellig falschen Sprung).
// Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per // Offset = Summe echter Hoehen (aus itemHeights-Cache, gefuettert per
// onLayout) + Fallback AVG fuer noch nicht gemessene. Bei „cold start" // onLayout) + dynamischer Fallback aus dem Mittel der bisher
// ist der Cache leer → AVG fuer alle → grob. Beim zweiten Such-Versuch // gemessenen Items. Beim Cold-Start gibt's nur 10 Messungen (die
// sind die Bubbles in der Naehe gemessen → genauer. // 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; let preOffset = 0;
const inv = invertedMessagesRef.current; const inv = invertedMessagesRef.current;
for (let i = 0; i < idx; i++) { 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 { try {
flatListRef.current?.scrollToOffset({ flatListRef.current?.scrollToOffset({
@@ -1473,9 +1481,10 @@ const ChatScreen: React.FC = () => {
animated: false, animated: false,
}); });
} catch {} } catch {}
// Nach kurzer Render-Pause praezise nachsetzen. 200 ms statt 80 ms — // Nach Render-Pause praezise nachsetzen. 350 ms — bei weiten Spruengen
// bei Cold-Start braucht FlatList laenger fuer das Item-Layout, das // (Pre-Scroll 5000+ px) braucht FlatList Zeit die Items dort zu
// war Stefans „erst beim zweiten Versuch klappt's"-Bug. // mounten und onLayout zu feuern. Zu kurz → averageItemLength im
// Failed-Handler basiert noch auf den falschen Items.
requestAnimationFrame(() => { requestAnimationFrame(() => {
setTimeout(() => { setTimeout(() => {
try { try {
@@ -1483,7 +1492,7 @@ const ChatScreen: React.FC = () => {
} catch { } catch {
// onScrollToIndexFailed-Handler uebernimmt den Fallback // onScrollToIndexFailed-Handler uebernimmt den Fallback
} }
}, 200); }, 350);
}); });
}, [searchIndex, searchMatchIds]); }, [searchIndex, searchMatchIds]);
@@ -2411,19 +2420,19 @@ const ChatScreen: React.FC = () => {
transparent transparent
onRequestClose={() => setThoughtsVisible(false)} onRequestClose={() => setThoughtsVisible(false)}
> >
<TouchableOpacity <View style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}}>
style={{flex:1, backgroundColor:'rgba(0,0,0,0.5)', justifyContent:'flex-end'}} {/* Tap-Outside-Bereich oberhalb des Sheets — separater Touchable
activeOpacity={1} damit das Sheet-View NICHT als Responder den FlatList-Scroll
onPress={() => setThoughtsVisible(false)} blockiert. Frueher hatten wir den ganzen Hintergrund als
> TouchableOpacity + inneren View mit onStartShouldSetResponder
{/* View statt TouchableOpacity, sonst konsumiert das die Touch- = das hat alle Touch-Events kassiert. */}
Events und die FlatList laesst sich nicht scrollen. <TouchableOpacity
onStartShouldSetResponder={true} blockt aber die Propagation style={{flex:1}}
an das aeussere TouchableOpacity (close-on-tap-outside). */} activeOpacity={1}
onPress={() => setThoughtsVisible(false)}
/>
<View <View
style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}} style={{height:'60%', backgroundColor:'#0D0D1A', borderTopLeftRadius:16, borderTopRightRadius:16}}
onStartShouldSetResponder={() => true}
onResponderTerminationRequest={() => false}
> >
{/* Drag-Indicator */} {/* Drag-Indicator */}
<View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}> <View style={{alignItems:'center', paddingTop:8, paddingBottom:4}}>
@@ -2504,7 +2513,7 @@ const ChatScreen: React.FC = () => {
/> />
)} )}
</View> </View>
</TouchableOpacity> </View>
</Modal> </Modal>
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles). {/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).