|
|
|
@@ -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,6 +1429,11 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
// Den aktuellen Snapshot von invertedMessages holen wir via Ref.
|
|
|
|
|
const invertedMessagesRef = useRef(invertedMessages);
|
|
|
|
|
invertedMessagesRef.current = invertedMessages;
|
|
|
|
|
// 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 = '';
|
|
|
|
@@ -1442,12 +1451,34 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
clearPendingScrollRetry();
|
|
|
|
|
const idx = invertedMessagesRef.current.findIndex(m => m.id === id);
|
|
|
|
|
if (idx < 0 || !flatListRef.current) return;
|
|
|
|
|
// 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: preOffset,
|
|
|
|
|
animated: false,
|
|
|
|
|
});
|
|
|
|
|
} catch {}
|
|
|
|
|
// Nach kurzer Render-Pause praezise nachsetzen
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
try {
|
|
|
|
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
|
|
|
|
|
} catch {
|
|
|
|
|
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
|
|
|
|
}
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
|
|
|
|
|
} catch {
|
|
|
|
|
// onScrollToIndexFailed-Handler uebernimmt den Fallback
|
|
|
|
|
}
|
|
|
|
|
}, 80);
|
|
|
|
|
});
|
|
|
|
|
}, [searchIndex, searchMatchIds]);
|
|
|
|
|
|
|
|
|
@@ -1865,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}>
|
|
|
|
@@ -2159,6 +2198,13 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
ref={flatListRef}
|
|
|
|
|
inverted
|
|
|
|
|
data={invertedMessages}
|
|
|
|
|
// Mehr Items beim Mount messen → bessere averageItemLength fuer
|
|
|
|
|
// Such-Sprung gleich nach App-Start. Default sind 10 Items, das
|
|
|
|
|
// ist bei 300+ Bubbles im Backup viel zu wenig.
|
|
|
|
|
initialNumToRender={30}
|
|
|
|
|
// Mehr Items im Speicher halten (Default 21 = 10 oben + 10 unten).
|
|
|
|
|
// Macht scroll-to-far-away weniger anfaellig fuer Layout-Holes.
|
|
|
|
|
windowSize={41}
|
|
|
|
|
onScroll={(e) => {
|
|
|
|
|
// Bei inverted FlatList: contentOffset.y > 0 = weg von "unten"
|
|
|
|
|
// (= aelter scrollen). Wir zeigen den Jump-Down-Button ab ~250px.
|
|
|
|
|