diff --git a/apps/rebreak-native/app/dm.tsx b/apps/rebreak-native/app/dm.tsx index 9127987..a0d9a4b 100644 --- a/apps/rebreak-native/app/dm.tsx +++ b/apps/rebreak-native/app/dm.tsx @@ -75,7 +75,6 @@ export default function DmScreen() { const { userId } = useLocalSearchParams<{ userId: string }>(); const flatListRef = useRef>(null); - const isNearBottomRef = useRef(true); const [messages, setMessages] = useState([]); const [partner, setPartner] = useState(null); const partnerRef = useRef(null); @@ -163,6 +162,7 @@ export default function DmScreen() { })); setMessages(msgs); requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: false })); + setTimeout(() => flatListRef.current?.scrollToEnd({ animated: false }), 100); return data; } catch (err: any) { console.error('[dm] history fetch failed:', err?.message ?? err); @@ -174,12 +174,10 @@ export default function DmScreen() { gcTime: 0, }); - // Neue Nachricht (incoming Realtime oder outgoing send) — nur scrollen wenn nahe unten + // Neue Nachricht (incoming Realtime oder outgoing send) → immer scrollen useEffect(() => { if (messages.length === 0) return; - if (isNearBottomRef.current) { - requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: true })); - } + requestAnimationFrame(() => flatListRef.current?.scrollToEnd({ animated: true })); }, [messages.length]); // Realtime: neue DMs vom Partner @@ -234,13 +232,12 @@ export default function DmScreen() { } } - async function uploadAttachment(): Promise<{ url: string; type: string; name: string } | null> { - if (!attachment) return null; + async function uploadAttachment(file: { uri: string; name: string }): Promise<{ url: string; type: string; name: string } | null> { try { 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 base64 = await FileSystem.readAsStringAsync(attachment.uri, { + const base64 = await FileSystem.readAsStringAsync(file.uri, { encoding: FileSystem.EncodingType.Base64, }); 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; 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) { Alert.alert(t('chat.upload_failed'), err?.message ?? ''); return null; @@ -266,58 +263,91 @@ export default function DmScreen() { const content = inputText.trim(); if (!content && !attachment) 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); + try { let attachmentMeta: { url: string; type: string; name: string } | null = null; - if (attachment) { - attachmentMeta = await uploadAttachment(); - if (!attachmentMeta) { setSending(false); return; } + if (currentAttachment) { + attachmentMeta = await uploadAttachment(currentAttachment); + if (!attachmentMeta) { + setMessages((prev) => + prev.map((m) => (m.id === tempId ? { ...m, status: 'failed' as const } : m)), + ); + setSending(false); + return; + } } + const newMsg = await apiFetch('/api/chat/dm', { method: 'POST', body: { receiverId: userId, content, - replyToId: replyTo?.id, + replyToId: currentReplyTo?.id, attachmentUrl: attachmentMeta?.url, attachmentType: attachmentMeta?.type, attachmentName: attachmentMeta?.name, }, }); - setMessages((prev) => [ - ...prev, - { - id: newMsg.id, - userId: myUserId ?? '', - nickname: 'Du', - avatar: null, - content: newMsg.content, - replyTo: newMsg.replyTo + + setMessages((prev) => + prev.map((m) => + m.id === tempId ? { - id: newMsg.replyTo.id, - userId: newMsg.replyTo.senderId, - nickname: - newMsg.replyTo.senderId === myUserId ? 'Du' : partner?.nickname ?? '?', - content: newMsg.replyTo.content?.slice(0, 100) ?? '', - attachmentType: newMsg.replyTo.attachmentType ?? null, + ...m, + id: newMsg.id, + content: newMsg.content, + attachmentUrl: newMsg.attachmentUrl ?? null, + attachmentType: newMsg.attachmentType ?? null, + attachmentName: newMsg.attachmentName ?? null, + createdAt: newMsg.createdAt, + status: 'sent' as const, } - : null, - attachmentUrl: newMsg.attachmentUrl, - attachmentType: newMsg.attachmentType, - attachmentName: newMsg.attachmentName, - likesCount: newMsg.likesCount ?? 0, - likedByMe: false, - createdAt: newMsg.createdAt, - isOwn: true, - readAt: null, - }, - ]); - setInputText(''); - setAttachment(null); - setReplyTo(null); + : m, + ), + ); + queryClient.invalidateQueries({ queryKey: ['dm-conversations'] }); } catch (err) { console.error('DM send failed:', err); + setMessages((prev) => + prev.map((m) => (m.id === tempId ? { ...m, status: 'failed' as const } : m)), + ); } finally { setSending(false); } @@ -491,17 +521,7 @@ export default function DmScreen() { showsVerticalScrollIndicator={false} keyboardDismissMode="interactive" keyboardShouldPersistTaps="handled" - onScroll={(e) => { - 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 }); - } - }} + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: false })} /> )}