fix(dm): Android scroll-to-bottom via scrollToOffset(999999)

scrollToEnd() unterschätzt Content-Höhe auf Android und stoppt
konsistent eine Message zu früh (verifiziert per adb-Screenshot).
scrollToOffset({offset:999999}) wird auf den echten Max-Wert geclampt
und landet immer am absoluten Ende der Liste.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-06-01 10:42:44 +02:00
parent 89391a807b
commit bd8d5a3072

View File

@ -79,6 +79,16 @@ export default function DmScreen() {
const { userId } = useLocalSearchParams<{ userId: string }>();
const flatListRef = useRef<FlatListType<ChatMsg>>(null);
// scrollToEnd() auf Android unterschätzt Content-Höhe und stoppt 1 Item
// zu früh. scrollToOffset(999999) wird auf den echten Max-Wert geclampt.
const scrollToBottom = useCallback((animated = false) => {
if (Platform.OS === 'android') {
flatListRef.current?.scrollToOffset({ offset: 999999, animated });
} else {
flatListRef.current?.scrollToEnd({ animated });
}
}, []);
const [messages, setMessages] = useState<ChatMsg[]>([]);
const [partner, setPartner] = useState<DmHistoryResponse['partner'] | null>(null);
const partnerRef = useRef<DmHistoryResponse['partner'] | null>(null);
@ -110,12 +120,12 @@ export default function DmScreen() {
const show = Keyboard.addListener(showEvent, (e) => {
setKeyboardHeight(e.endCoordinates.height);
setKeyboardVisible(true);
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: false }));
requestAnimationFrame(() => scrollToBottom(false));
});
const hide = Keyboard.addListener(hideEvent, () => {
setKeyboardHeight(0);
setKeyboardVisible(false);
setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 50);
setTimeout(() => scrollToBottom(false), 50);
});
return () => { show.remove(); hide.remove(); };
}, []);
@ -170,9 +180,9 @@ export default function DmScreen() {
setMessages(msgs);
// Dreistufiges Scroll-to-bottom: rAF + 100ms + 300ms deckt
// Fälle ab wo Bilder nachgeladen werden und Content-Höhe wächst.
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: false }));
setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 100);
setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 300);
requestAnimationFrame(() => scrollToBottom(false));
setTimeout(() => scrollToBottom(false), 100);
setTimeout(() => scrollToBottom(false), 300);
return data;
} catch (err: any) {
console.error('[dm] history fetch failed:', err?.message ?? err);
@ -187,7 +197,7 @@ export default function DmScreen() {
// Neue Nachricht (incoming Realtime oder outgoing send) → immer scrollen
useEffect(() => {
if (messages.length === 0) return;
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: true }));
requestAnimationFrame(() => scrollToBottom(true));
}, [messages.length]);
// Realtime: neue DMs vom Partner
@ -545,8 +555,8 @@ export default function DmScreen() {
showsVerticalScrollIndicator={false}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })}
onLayout={() => flatListRef.current?.scrollToEnd({ animated: false })}
onContentSizeChange={() => scrollToBottom(false)}
onLayout={() => scrollToBottom(false)}
/>
)}
</View>