fix(chat): Such-Sprung praezise via Layout-Cache + Filter
Symptom: Suche nach 'cessna' sprang zur Oberhausen-Bubble (~15 Bubbles daneben), egal welcher Versuch. Zwei Ursachen: 1) searchMatchIds suchte in `messages` (alle Bubbles inkl. Memory/Skill/ Trigger-Spezial-Bubbles), aber gescrollt wird in `invertedMessages` die diese filtert. Wenn 'cessna' nur in einer Memory-Bubble vorkam, war die ID in searchMatchIds aber nicht in invertedMessages → findIndex=-1 → kein Scroll, Pre-Scroll-Offset von voriger Aktion blieb sichtbar. Fix: searchMatchIds aus chatVisibleMessages. 2) AVG_BUBBLE_HEIGHT=150 als Pauschalschaetzung war zu grob — Voice- Bubbles sind ~70 px, lange ARIA-Antworten 400+. Pre-Scroll-Offset landete bei langen Listen weit daneben. Fix: itemHeights-Ref-Map wird per onLayout in renderMessage gefuettert. Pre-Scroll summiert echte gemessene Hoehen (Fallback AVG fuer noch nicht gerenderte) — beim zweiten Such-Versuch lernt der Cache, beim ersten klappt's schon besser als mit dem Pauschalwert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1383,13 +1383,17 @@ const ChatScreen: React.FC = () => {
|
||||
|
||||
// Such-Treffer: alle Message-IDs die zur Query passen, in chronologischer
|
||||
// Reihenfolge (aelteste zuerst). Bei Query-Change resetten wir den Index.
|
||||
// WICHTIG: nur in chatVisibleMessages suchen — Spezial-Bubbles (Memory/
|
||||
// Skill/Trigger) sind im Chat-Stream nicht sichtbar und Treffer auf die
|
||||
// wuerden zu „ID nicht im FlatList → findIndex=-1 → kein Scroll"-Fail
|
||||
// fuehren (Cessna in einer Memory-Bubble → springt zur falschen Stelle).
|
||||
const searchMatchIds = useMemo(() => {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
if (!q) return [] as string[];
|
||||
return messages
|
||||
return chatVisibleMessages
|
||||
.filter(m => (m.text || '').toLowerCase().includes(q))
|
||||
.map(m => m.id);
|
||||
}, [messages, searchQuery]);
|
||||
}, [chatVisibleMessages, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchIndex(0);
|
||||
@@ -1425,11 +1429,11 @@ const ChatScreen: React.FC = () => {
|
||||
// Den aktuellen Snapshot von invertedMessages holen wir via Ref.
|
||||
const invertedMessagesRef = useRef(invertedMessages);
|
||||
invertedMessagesRef.current = invertedMessages;
|
||||
// Schaetzwert fuer durchschnittliche Bubble-Hoehe. Wird fuer den
|
||||
// ersten Pre-Scroll genutzt wenn FlatList noch keine Layouts hat.
|
||||
// 150 px ist Mittel zwischen kurzen Voice-Bubbles (~70) und langen
|
||||
// ARIA-Antworten (~250).
|
||||
const AVG_BUBBLE_HEIGHT = 150;
|
||||
// Cache fuer echte Bubble-Hoehen, gefuettert per onLayout in
|
||||
// renderMessage. Wird beim Pre-Scroll genutzt damit der grobe Sprung
|
||||
// praezise landet (statt mit dem 150-px-Pauschalwert weit daneben).
|
||||
const itemHeights = useRef<Map<string, number>>(new Map());
|
||||
const AVG_BUBBLE_HEIGHT = 150; // Fallback fuer noch nicht gemessene Items
|
||||
useEffect(() => {
|
||||
if (!searchMatchIds.length) {
|
||||
lastSearchScrollKey.current = '';
|
||||
@@ -1447,15 +1451,22 @@ const ChatScreen: React.FC = () => {
|
||||
clearPendingScrollRetry();
|
||||
const idx = invertedMessagesRef.current.findIndex(m => m.id === id);
|
||||
if (idx < 0 || !flatListRef.current) return;
|
||||
// Pre-Scroll: erst grob mit konstanter Hoehe in die Naehe springen.
|
||||
// Damit hat FlatList ueberhaupt die Chance die Layouts der Bubbles in
|
||||
// der Naehe zu rendern — sonst basiert info.averageItemLength im
|
||||
// Failed-Handler nur auf den ersten 10 Items (Default initialNumToRender)
|
||||
// und liefert beim ersten Such-Versuch nach App-Start einen voellig
|
||||
// falschen Sprung.
|
||||
// Pre-Scroll: erst grob in die Naehe springen, damit FlatList die
|
||||
// Bubbles in der Umgebung ueberhaupt rendert (sonst basiert
|
||||
// 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.
|
||||
let preOffset = 0;
|
||||
const inv = invertedMessagesRef.current;
|
||||
for (let i = 0; i < idx; i++) {
|
||||
preOffset += itemHeights.current.get(inv[i].id) || AVG_BUBBLE_HEIGHT;
|
||||
}
|
||||
try {
|
||||
flatListRef.current?.scrollToOffset({
|
||||
offset: idx * AVG_BUBBLE_HEIGHT,
|
||||
offset: preOffset,
|
||||
animated: false,
|
||||
});
|
||||
} catch {}
|
||||
@@ -1885,7 +1896,15 @@ const ChatScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}>
|
||||
<View
|
||||
style={[styles.messageBubble, isUser ? styles.userBubble : styles.ariaBubble, searchHighlightStyle]}
|
||||
onLayout={e => {
|
||||
// Echte Hoehe in Cache speichern — Pre-Scroll der Suche nutzt
|
||||
// die summierten Cache-Werte fuer praezisen Sprung. Bei
|
||||
// unbekannten Items faellt's auf AVG_BUBBLE_HEIGHT zurueck.
|
||||
itemHeights.current.set(item.id, e.nativeEvent.layout.height);
|
||||
}}
|
||||
>
|
||||
{/* Anhang-Vorschau */}
|
||||
{item.attachments?.map((att, idx) => (
|
||||
<View key={idx}>
|
||||
|
||||
Reference in New Issue
Block a user