|
|
|
@@ -236,6 +236,7 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
const [fullscreenImage, setFullscreenImage] = useState<string | null>(null);
|
|
|
|
|
const [memoryDetailId, setMemoryDetailId] = useState<string | null>(null);
|
|
|
|
|
const [inboxVisible, setInboxVisible] = useState(false);
|
|
|
|
|
const [showJumpDown, setShowJumpDown] = useState(false);
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [searchVisible, setSearchVisible] = useState(false);
|
|
|
|
|
const [searchIndex, setSearchIndex] = useState(0); // welcher Treffer aktiv ist
|
|
|
|
@@ -1052,9 +1053,10 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
}, [searchQuery]);
|
|
|
|
|
|
|
|
|
|
// Bei Index-Wechsel zu der entsprechenden Bubble scrollen.
|
|
|
|
|
// FlatList ist `inverted` → viewPosition 0.5 (mitte) ist beim inverted-Render
|
|
|
|
|
// tatsaechlich die Mitte des sichtbaren Bereichs. Wir verzoegern minimal
|
|
|
|
|
// damit Layout sicher fertig ist.
|
|
|
|
|
// FlatList ist `inverted`. viewPosition 0 = Item-Top oben am Viewport →
|
|
|
|
|
// Treffer-Bubble liegt mit dem Anfang direkt oben sichtbar, kein
|
|
|
|
|
// weiteres Hochscrollen noetig. Plus mehrere Retries da Layout bei
|
|
|
|
|
// langen Listen zeitversetzt fertig wird.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!searchMatchIds.length) return;
|
|
|
|
|
const id = searchMatchIds[searchIndex];
|
|
|
|
@@ -1063,13 +1065,16 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
if (idx < 0 || !flatListRef.current) return;
|
|
|
|
|
const tryScroll = () => {
|
|
|
|
|
try {
|
|
|
|
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0.5 });
|
|
|
|
|
flatListRef.current?.scrollToIndex({ index: idx, animated: true, viewPosition: 0 });
|
|
|
|
|
} catch {
|
|
|
|
|
// wird von onScrollToIndexFailed nochmal versucht
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// requestAnimationFrame statt setTimeout 0 — wartet auf naechsten Layout-Frame
|
|
|
|
|
// requestAnimationFrame fuer den ersten Versuch, dann setTimeout-Folge
|
|
|
|
|
// damit auch bei tiefen Indizes (viel ungelayoutete Items dazwischen)
|
|
|
|
|
// der Sprung am Ende sitzt.
|
|
|
|
|
requestAnimationFrame(tryScroll);
|
|
|
|
|
[180, 420, 800].forEach(d => setTimeout(tryScroll, d));
|
|
|
|
|
}, [searchIndex, searchMatchIds, invertedMessages]);
|
|
|
|
|
|
|
|
|
|
const activeSearchId = searchMatchIds[searchIndex] || '';
|
|
|
|
@@ -1726,15 +1731,27 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
ref={flatListRef}
|
|
|
|
|
inverted
|
|
|
|
|
data={invertedMessages}
|
|
|
|
|
onScroll={(e) => {
|
|
|
|
|
// Bei inverted FlatList: contentOffset.y > 0 = weg von "unten"
|
|
|
|
|
// (= aelter scrollen). Wir zeigen den Jump-Down-Button ab ~250px.
|
|
|
|
|
const y = e.nativeEvent.contentOffset.y;
|
|
|
|
|
setShowJumpDown(y > 250);
|
|
|
|
|
}}
|
|
|
|
|
scrollEventThrottle={120}
|
|
|
|
|
onScrollToIndexFailed={(info) => {
|
|
|
|
|
// FlatList kennt das Item-Layout noch nicht. Zuerst grob in die
|
|
|
|
|
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann nach 250ms
|
|
|
|
|
// praezise nochmal versuchen.
|
|
|
|
|
// Naehe scrollen (Average-Item-Hoehe-Schaetzung), dann mehrfach
|
|
|
|
|
// praezise nachsetzen — bei langem Chat braucht's manchmal mehrere
|
|
|
|
|
// Runden bis die Layouts gemessen sind.
|
|
|
|
|
const offset = info.averageItemLength * info.index;
|
|
|
|
|
try { flatListRef.current?.scrollToOffset({ offset, animated: false }); } catch {}
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0.5 }); } catch {}
|
|
|
|
|
}, 250);
|
|
|
|
|
// viewPosition 0 = Item-Top oben am Viewport → Stefan landet am
|
|
|
|
|
// Text-Anfang der Bubble, nicht in der Mitte oder am Ende.
|
|
|
|
|
[120, 320, 600].forEach(delay => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try { flatListRef.current?.scrollToIndex({ index: info.index, animated: true, viewPosition: 0 }); } catch {}
|
|
|
|
|
}, delay);
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
keyExtractor={item => item.id}
|
|
|
|
|
renderItem={renderMessage}
|
|
|
|
@@ -1801,6 +1818,24 @@ const ChatScreen: React.FC = () => {
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Jump-to-Bottom-Button — erscheint wenn man weg von der neuesten
|
|
|
|
|
Nachricht gescrollt hat. Bei inverted FlatList ist scrollToOffset
|
|
|
|
|
0 == neueste Nachricht visuell unten. */}
|
|
|
|
|
{showJumpDown && (
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
style={styles.jumpDownBtn}
|
|
|
|
|
activeOpacity={0.85}
|
|
|
|
|
onPress={() => {
|
|
|
|
|
try {
|
|
|
|
|
flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
|
|
|
|
} catch {}
|
|
|
|
|
setShowJumpDown(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text style={{color:'#fff', fontSize:18, fontWeight:'700'}}>{'↓'}</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Eingabebereich */}
|
|
|
|
|
<View style={styles.inputContainer}>
|
|
|
|
|
{/* Datei-Buttons */}
|
|
|
|
@@ -2341,6 +2376,23 @@ const styles = StyleSheet.create({
|
|
|
|
|
color: '#555570',
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
},
|
|
|
|
|
jumpDownBtn: {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
right: 16,
|
|
|
|
|
bottom: 80,
|
|
|
|
|
width: 44,
|
|
|
|
|
height: 44,
|
|
|
|
|
borderRadius: 22,
|
|
|
|
|
backgroundColor: '#0096FF',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
shadowColor: '#000',
|
|
|
|
|
shadowOffset: { width: 0, height: 2 },
|
|
|
|
|
shadowOpacity: 0.4,
|
|
|
|
|
shadowRadius: 4,
|
|
|
|
|
elevation: 5,
|
|
|
|
|
zIndex: 100,
|
|
|
|
|
},
|
|
|
|
|
bubbleTrash: {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 4,
|
|
|
|
|