fix(dm): always scroll to bottom on new messages and content-size changes
The smart isNearBottomRef gating was too restrictive — own sent messages, image-loads, and incoming partner messages were sometimes not scrolled to. Adopt the room-chat pattern: always scroll on messages.length change and onContentSizeChange. Drop isNearBottomRef + firstContentSizeChangeRef + onScroll handler.
This commit is contained in:
parent
55e3cdfb26
commit
49e558902b
@ -75,7 +75,6 @@ export default function DmScreen() {
|
|||||||
const { userId } = useLocalSearchParams<{ userId: string }>();
|
const { userId } = useLocalSearchParams<{ userId: string }>();
|
||||||
|
|
||||||
const flatListRef = useRef<FlatListType<ChatMsg>>(null);
|
const flatListRef = useRef<FlatListType<ChatMsg>>(null);
|
||||||
const isNearBottomRef = useRef(true);
|
|
||||||
const [messages, setMessages] = useState<ChatMsg[]>([]);
|
const [messages, setMessages] = useState<ChatMsg[]>([]);
|
||||||
const [partner, setPartner] = useState<DmHistoryResponse['partner'] | null>(null);
|
const [partner, setPartner] = useState<DmHistoryResponse['partner'] | null>(null);
|
||||||
const partnerRef = useRef<DmHistoryResponse['partner'] | null>(null);
|
const partnerRef = useRef<DmHistoryResponse['partner'] | null>(null);
|
||||||
@ -163,6 +162,7 @@ export default function DmScreen() {
|
|||||||
}));
|
}));
|
||||||
setMessages(msgs);
|
setMessages(msgs);
|
||||||
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: false }));
|
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: false }));
|
||||||
|
setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 100);
|
||||||
return data;
|
return data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[dm] history fetch failed:', err?.message ?? err);
|
console.error('[dm] history fetch failed:', err?.message ?? err);
|
||||||
@ -174,12 +174,10 @@ export default function DmScreen() {
|
|||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Neue Nachricht (incoming Realtime oder outgoing send) — nur scrollen wenn nahe unten
|
// Neue Nachricht (incoming Realtime oder outgoing send) → immer scrollen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length === 0) return;
|
if (messages.length === 0) return;
|
||||||
if (isNearBottomRef.current) {
|
|
||||||
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: true }));
|
requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: true }));
|
||||||
}
|
|
||||||
}, [messages.length]);
|
}, [messages.length]);
|
||||||
|
|
||||||
// Realtime: neue DMs vom Partner
|
// Realtime: neue DMs vom Partner
|
||||||
@ -234,13 +232,12 @@ export default function DmScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadAttachment(): Promise<{ url: string; type: string; name: string } | null> {
|
async function uploadAttachment(file: { uri: string; name: string }): Promise<{ url: string; type: string; name: string } | null> {
|
||||||
if (!attachment) return null;
|
|
||||||
try {
|
try {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
const ext = attachment.name.split('.').pop() || 'jpg';
|
const ext = file.name.split('.').pop() || 'jpg';
|
||||||
const path = `chat/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
const path = `chat/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
|
||||||
const base64 = await FileSystem.readAsStringAsync(attachment.uri, {
|
const base64 = await FileSystem.readAsStringAsync(file.uri, {
|
||||||
encoding: FileSystem.EncodingType.Base64,
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
});
|
});
|
||||||
const binary = typeof atob === 'function' ? atob(base64) : Buffer.from(base64, 'base64').toString('binary');
|
const binary = typeof atob === 'function' ? atob(base64) : Buffer.from(base64, 'base64').toString('binary');
|
||||||
@ -253,7 +250,7 @@ export default function DmScreen() {
|
|||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
const { data } = supabase.storage.from('chat-attachments').getPublicUrl(path);
|
const { data } = supabase.storage.from('chat-attachments').getPublicUrl(path);
|
||||||
return { url: data.publicUrl, type: 'image', name: attachment.name };
|
return { url: data.publicUrl, type: 'image', name: file.name };
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Alert.alert(t('chat.upload_failed'), err?.message ?? '');
|
Alert.alert(t('chat.upload_failed'), err?.message ?? '');
|
||||||
return null;
|
return null;
|
||||||
@ -266,58 +263,91 @@ export default function DmScreen() {
|
|||||||
const content = inputText.trim();
|
const content = inputText.trim();
|
||||||
if (!content && !attachment) return;
|
if (!content && !attachment) return;
|
||||||
if (sending || uploading) return;
|
if (sending || uploading) return;
|
||||||
|
|
||||||
|
const tempId = `temp-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
const currentReplyTo = replyTo;
|
||||||
|
const currentAttachment = attachment;
|
||||||
|
|
||||||
|
const optimisticMsg: ChatMsg = {
|
||||||
|
id: tempId,
|
||||||
|
userId: myUserId ?? '',
|
||||||
|
nickname: 'Du',
|
||||||
|
avatar: null,
|
||||||
|
content,
|
||||||
|
replyTo: currentReplyTo
|
||||||
|
? {
|
||||||
|
id: currentReplyTo.id,
|
||||||
|
userId: myUserId ?? '',
|
||||||
|
nickname: currentReplyTo.nickname,
|
||||||
|
content: currentReplyTo.content,
|
||||||
|
attachmentType: null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
attachmentUrl: currentAttachment?.uri ?? null,
|
||||||
|
attachmentType: currentAttachment ? 'image' : null,
|
||||||
|
attachmentName: currentAttachment?.name ?? null,
|
||||||
|
likesCount: 0,
|
||||||
|
likedByMe: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
isOwn: true,
|
||||||
|
readAt: null,
|
||||||
|
status: 'pending',
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, optimisticMsg]);
|
||||||
|
setInputText('');
|
||||||
|
setAttachment(null);
|
||||||
|
setReplyTo(null);
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let attachmentMeta: { url: string; type: string; name: string } | null = null;
|
let attachmentMeta: { url: string; type: string; name: string } | null = null;
|
||||||
if (attachment) {
|
if (currentAttachment) {
|
||||||
attachmentMeta = await uploadAttachment();
|
attachmentMeta = await uploadAttachment(currentAttachment);
|
||||||
if (!attachmentMeta) { setSending(false); return; }
|
if (!attachmentMeta) {
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((m) => (m.id === tempId ? { ...m, status: 'failed' as const } : m)),
|
||||||
|
);
|
||||||
|
setSending(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newMsg = await apiFetch<any>('/api/chat/dm', {
|
const newMsg = await apiFetch<any>('/api/chat/dm', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
receiverId: userId,
|
receiverId: userId,
|
||||||
content,
|
content,
|
||||||
replyToId: replyTo?.id,
|
replyToId: currentReplyTo?.id,
|
||||||
attachmentUrl: attachmentMeta?.url,
|
attachmentUrl: attachmentMeta?.url,
|
||||||
attachmentType: attachmentMeta?.type,
|
attachmentType: attachmentMeta?.type,
|
||||||
attachmentName: attachmentMeta?.name,
|
attachmentName: attachmentMeta?.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setMessages((prev) => [
|
|
||||||
...prev,
|
setMessages((prev) =>
|
||||||
{
|
prev.map((m) =>
|
||||||
id: newMsg.id,
|
m.id === tempId
|
||||||
userId: myUserId ?? '',
|
|
||||||
nickname: 'Du',
|
|
||||||
avatar: null,
|
|
||||||
content: newMsg.content,
|
|
||||||
replyTo: newMsg.replyTo
|
|
||||||
? {
|
? {
|
||||||
id: newMsg.replyTo.id,
|
...m,
|
||||||
userId: newMsg.replyTo.senderId,
|
id: newMsg.id,
|
||||||
nickname:
|
content: newMsg.content,
|
||||||
newMsg.replyTo.senderId === myUserId ? 'Du' : partner?.nickname ?? '?',
|
attachmentUrl: newMsg.attachmentUrl ?? null,
|
||||||
content: newMsg.replyTo.content?.slice(0, 100) ?? '',
|
attachmentType: newMsg.attachmentType ?? null,
|
||||||
attachmentType: newMsg.replyTo.attachmentType ?? null,
|
attachmentName: newMsg.attachmentName ?? null,
|
||||||
}
|
|
||||||
: null,
|
|
||||||
attachmentUrl: newMsg.attachmentUrl,
|
|
||||||
attachmentType: newMsg.attachmentType,
|
|
||||||
attachmentName: newMsg.attachmentName,
|
|
||||||
likesCount: newMsg.likesCount ?? 0,
|
|
||||||
likedByMe: false,
|
|
||||||
createdAt: newMsg.createdAt,
|
createdAt: newMsg.createdAt,
|
||||||
isOwn: true,
|
status: 'sent' as const,
|
||||||
readAt: null,
|
}
|
||||||
},
|
: m,
|
||||||
]);
|
),
|
||||||
setInputText('');
|
);
|
||||||
setAttachment(null);
|
|
||||||
setReplyTo(null);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['dm-conversations'] });
|
queryClient.invalidateQueries({ queryKey: ['dm-conversations'] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('DM send failed:', err);
|
console.error('DM send failed:', err);
|
||||||
|
setMessages((prev) =>
|
||||||
|
prev.map((m) => (m.id === tempId ? { ...m, status: 'failed' as const } : m)),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
@ -491,17 +521,7 @@ export default function DmScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
keyboardDismissMode="interactive"
|
keyboardDismissMode="interactive"
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
onScroll={(e) => {
|
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })}
|
||||||
const { layoutMeasurement, contentOffset, contentSize } = e.nativeEvent;
|
|
||||||
const distFromBottom = contentSize.height - contentOffset.y - layoutMeasurement.height;
|
|
||||||
isNearBottomRef.current = distFromBottom < 80;
|
|
||||||
}}
|
|
||||||
scrollEventThrottle={100}
|
|
||||||
onContentSizeChange={() => {
|
|
||||||
if (isNearBottomRef.current) {
|
|
||||||
flatListRef.current?.scrollToEnd({ animated: false });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user