fix(chat): Conversation auf inverted FlatList — Scroll-to-bottom bulletproof

Der setTimeout(80)+onImageLoad-Ansatz war ein Timing-Hack gegen ein
strukturelles Problem (lazy Item-Measurement unter Fabric -> scrollToEnd
landet zu kurz). Stattdessen jetzt inverted FlatList: Index 0 sitzt
permanent am Bildschirmrand, neueste Nachricht immer sichtbar.

- dm.tsx: inverted + reversedMessages, Gruppen-Logik gespiegelt,
  manuellen Auto-Scroll + keyboardHeight-State entfernt
- ChatBubble.tsx: onImageLoad-Prop entfernt (obsolet)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-22 20:33:34 +02:00
parent 517ce8658f
commit 48a8bbc4af
2 changed files with 8 additions and 40 deletions

View File

@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import {
View,
Text,
@ -7,7 +7,6 @@ import {
Platform,
ActivityIndicator,
StyleSheet,
Keyboard,
KeyboardAvoidingView,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
@ -59,7 +58,6 @@ export default function DmScreen() {
const colors = useColors();
const styles = makeStyles(colors);
const queryClient = useQueryClient();
const flatRef = useRef<FlatList>(null);
const myUserId = useAuthStore((s) => s.user?.id);
const colorScheme = useThemeStore((s) => s.colorScheme);
@ -67,7 +65,6 @@ export default function DmScreen() {
const { userId } = useLocalSearchParams<{ userId: string }>();
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [messages, setMessages] = useState<ChatMsg[]>([]);
const [partner, setPartner] = useState<DmHistoryResponse['partner'] | null>(null);
const partnerRef = useRef<DmHistoryResponse['partner'] | null>(null);
@ -76,24 +73,12 @@ export default function DmScreen() {
);
const [sending, setSending] = useState(false);
useEffect(() => {
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const show = Keyboard.addListener(showEvent, (e) => setKeyboardHeight(e.endCoordinates.height));
const hide = Keyboard.addListener(hideEvent, () => setKeyboardHeight(0));
return () => {
show.remove();
hide.remove();
};
}, []);
// Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse)
useEffect(() => {
setMessages([]);
setPartner(null);
partnerRef.current = null;
setReplyTo(null);
isInitialLoad.current = true;
}, [userId]);
// Lade DM-History — staleTime:0 erzwingt immer frischen Fetch (kein Cache-Hit-Bug)
@ -175,20 +160,7 @@ export default function DmScreen() {
);
useDmRealtime(userId, onDmInsert, !!myUserId);
const isInitialLoad = useRef(true);
const scrollToBottom = useCallback((animated: boolean) => {
setTimeout(() => flatRef.current?.scrollToEnd({ animated }), 80);
}, []);
// Auto-Scroll bei neuen Messages
useEffect(() => {
if (messages.length > 0) {
const animated = !isInitialLoad.current;
isInitialLoad.current = false;
scrollToBottom(animated);
}
}, [messages.length, scrollToBottom]);
const reversedMessages = useMemo(() => [...messages].reverse(), [messages]);
async function handleSend(payload: SendPayload) {
if (sending) return;
@ -308,28 +280,27 @@ export default function DmScreen() {
</View>
) : (
<FlatList
ref={flatRef}
data={messages}
inverted
data={reversedMessages}
style={{ flex: 1 }}
renderItem={({ item, index }) => (
<ChatBubble
msg={item}
isFirstInGroup={!sameAuthor(messages[index - 1], item)}
isLastInGroup={!sameAuthor(item, messages[index + 1])}
isFirstInGroup={!sameAuthor(item, reversedMessages[index + 1])}
isLastInGroup={!sameAuthor(reversedMessages[index - 1], item)}
onReply={startReply}
onLike={toggleLike}
onOpenImage={() => {}}
onImageLoad={index === messages.length - 1 ? () => scrollToBottom(false) : undefined}
/>
)}
keyExtractor={(m) => m.id}
contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }}
contentContainerStyle={{ paddingBottom: 12, paddingTop: 8 }}
showsVerticalScrollIndicator={false}
/>
)}
</View>
<View style={{ paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom), backgroundColor: colors.bg }}>
<View style={{ paddingBottom: Math.max(12, insets.bottom), backgroundColor: colors.bg }}>
<ChatInput
replyTo={replyTo}
sending={sending}

View File

@ -47,7 +47,6 @@ type Props = {
onReply: (msg: ChatMsg) => void;
onLike: (msg: ChatMsg) => void;
onOpenImage: (url: string) => void;
onImageLoad?: () => void;
};
function formatTime(ts: string) {
@ -76,7 +75,6 @@ export function ChatBubble({
onReply,
onLike,
onOpenImage,
onImageLoad,
}: Props) {
const { t } = useTranslation();
const colors = useColors();
@ -203,7 +201,6 @@ export function ChatBubble({
contentFit="cover"
cachePolicy="memory-disk"
transition={200}
onLoad={onImageLoad ? () => onImageLoad() : undefined}
/>
{isImageOnly && (
<View style={styles.imageTimeOverlay}>