import { useState, useRef, useEffect, useCallback } from 'react'; import { View, Text, FlatList, TouchableOpacity, KeyboardAvoidingView, Platform, ActivityIndicator, Image, StyleSheet, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useRouter, useLocalSearchParams } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { apiFetch } from '../lib/api'; import { supabase } from '../lib/supabase'; import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble'; import { resolveAvatar } from '../lib/resolveAvatar'; import { ChatInput, type SendPayload } from '../components/chat/ChatInput'; import { DmChatBackground } from '../components/chat/DmChatBackground'; import { useDmRealtime } from '../hooks/useChatRealtime'; import { useColors } from '../lib/theme'; import { useThemeStore } from '../stores/theme'; type DmHistoryResponse = { partner: { id: string; nickname: string; username?: string; avatar?: string | null; }; messages: Array<{ id: string; content: string; createdAt: string; isOwn: boolean; readAt: string | null; senderId?: string; receiverId?: string; likesCount?: number; likedByMe?: boolean; attachmentUrl?: string | null; attachmentType?: string | null; attachmentName?: string | null; replyTo?: any; }>; }; const GROUP_GAP_MS = 5 * 60 * 1000; export default function DmScreen() { const { t } = useTranslation(); const router = useRouter(); const insets = useSafeAreaInsets(); const colors = useColors(); const styles = makeStyles(colors); const queryClient = useQueryClient(); const flatRef = useRef(null); const [myUserId, setMyUserId] = useState(undefined); const colorScheme = useThemeStore((s) => s.colorScheme); const chatBg = colorScheme === 'dark' ? '#1a1f1e' : '#EDE8E1'; const { userId } = useLocalSearchParams<{ userId: string }>(); const [messages, setMessages] = useState([]); const [partner, setPartner] = useState(null); const partnerRef = useRef(null); const [replyTo, setReplyTo] = useState<{ id: string; nickname: string; content: string } | null>( null, ); const [sending, setSending] = useState(false); // Lade meine User-ID useEffect(() => { supabase.auth.getSession().then(({ data }) => { setMyUserId(data.session?.user.id); }); }, []); // Lade DM-History const { isLoading } = useQuery({ queryKey: ['dm-history', userId], queryFn: async () => { console.log('[dm] fetching history for partner', userId, 'me', myUserId); try { const data = await apiFetch(`/api/chat/dm/${userId}`); console.log('[dm] partner:', data.partner?.nickname, 'msgs:', data.messages?.length); setPartner(data.partner); partnerRef.current = data.partner; const msgs: ChatMsg[] = data.messages.map((m: any) => ({ id: m.id, userId: m.senderId ?? (m.isOwn ? myUserId ?? '' : userId), nickname: m.isOwn ? 'Du' : data.partner?.nickname ?? '?', avatar: m.isOwn ? null : data.partner?.avatar ?? null, content: m.content, replyTo: m.replyTo ? { id: m.replyTo.id, userId: m.replyTo.senderId, nickname: m.replyTo.senderId === myUserId ? 'Du' : data.partner?.nickname ?? '?', content: m.replyTo.content?.slice(0, 100) ?? '', attachmentType: m.replyTo.attachmentType ?? null, } : null, attachmentUrl: m.attachmentUrl ?? null, attachmentType: m.attachmentType ?? null, attachmentName: m.attachmentName ?? null, likesCount: m.likesCount ?? 0, likedByMe: m.likedByMe ?? false, createdAt: m.createdAt, isOwn: m.isOwn, readAt: m.readAt, })); setMessages(msgs); return data; } catch (err: any) { console.error('[dm] history fetch failed:', err?.message ?? err); throw err; } }, enabled: !!userId && !!myUserId, }); // Realtime: neue DMs vom Partner const onDmInsert = useCallback( (row: any) => { if (row.receiver_id !== myUserId) return; setMessages((prev) => { if (prev.some((m) => m.id === row.id)) return prev; const p = partnerRef.current; return [ ...prev, { id: row.id, userId: row.sender_id, nickname: p?.nickname ?? '?', avatar: p?.avatar ?? null, content: row.content ?? '', replyTo: null, attachmentUrl: row.attachment_url ?? null, attachmentType: row.attachment_type ?? null, attachmentName: row.attachment_name ?? null, likesCount: row.likes_count ?? 0, likedByMe: false, createdAt: row.created_at, isOwn: false, readAt: null, }, ]; }); }, [myUserId], ); useDmRealtime(userId, onDmInsert, !!myUserId); // Auto-Scroll bei neuen Messages useEffect(() => { if (messages.length > 0) { requestAnimationFrame(() => flatRef.current?.scrollToEnd({ animated: true })); } }, [messages.length]); async function handleSend(payload: SendPayload) { if (sending) return; setSending(true); try { const newMsg = await apiFetch('/api/chat/dm', { method: 'POST', body: { receiverId: userId, ...payload }, }); setMessages((prev) => [ ...prev, { id: newMsg.id, userId: myUserId ?? '', nickname: 'Du', avatar: null, content: newMsg.content, replyTo: newMsg.replyTo ? { 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, } : null, attachmentUrl: newMsg.attachmentUrl, attachmentType: newMsg.attachmentType, attachmentName: newMsg.attachmentName, likesCount: newMsg.likesCount ?? 0, likedByMe: false, createdAt: newMsg.createdAt, isOwn: true, readAt: null, }, ]); setReplyTo(null); queryClient.invalidateQueries({ queryKey: ['dm-conversations'] }); } catch (err) { console.error('DM send failed:', err); } finally { setSending(false); } } async function toggleLike(msg: ChatMsg) { try { const { liked } = await apiFetch<{ liked: boolean }>('/api/chat/like', { method: 'POST', body: { messageId: msg.id, type: 'dm' }, }); setMessages((prev) => prev.map((m) => m.id === msg.id ? { ...m, likedByMe: liked, likesCount: m.likesCount + (liked ? 1 : -1) } : m, ), ); } catch {} } function startReply(msg: ChatMsg) { setReplyTo({ id: msg.id, nickname: msg.nickname ?? '?', content: msg.content?.slice(0, 100) || (msg.attachmentType === 'image' ? 'Bild' : ''), }); } function sameAuthor(a: ChatMsg | undefined, b: ChatMsg | undefined): boolean { if (!a || !b) return false; if (a.userId !== b.userId) return false; return Math.abs(new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) <= GROUP_GAP_MS; } return ( {/* Header */} router.back()} hitSlop={8} activeOpacity={0.7}> {partner?.avatar ? ( ) : ( {(partner?.nickname ?? '?').slice(0, 2).toUpperCase()} )} {partner?.nickname ?? '…'} {isLoading && messages.length === 0 ? ( ) : messages.length === 0 ? ( {t('chat.no_chats')} ) : ( ( {}} /> )} keyExtractor={(m) => m.id} contentContainerStyle={{ paddingTop: 12, paddingBottom: 8 }} showsVerticalScrollIndicator={false} onContentSizeChange={() => flatRef.current?.scrollToEnd({ animated: false })} /> )} setReplyTo(null)} /> ); } function makeStyles(colors: ReturnType) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 10, backgroundColor: colors.bg, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, }, backBtn: { width: 36, height: 36, borderRadius: 12, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', }, headerCenter: { flex: 1, flexDirection: 'row', alignItems: 'center', marginLeft: 8, }, headerAvatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', marginRight: 8, }, headerAvatarImg: { width: 32, height: 32 }, headerAvatarInitials: { fontSize: 11, fontFamily: 'Nunito_700Bold', color: colors.textMuted, }, headerName: { fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text, flexShrink: 1, }, loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center', }, emptyText: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted, marginTop: 12, }, }); }