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:
parent
517ce8658f
commit
48a8bbc4af
@ -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}
|
||||
|
||||
@ -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}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user