import { useState, useCallback } from 'react'; import { View, Text, FlatList, TouchableOpacity, ActivityIndicator, Image, RefreshControl, StyleSheet, } from 'react-native'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { apiFetch } from '../../lib/api'; import { AppHeader } from '../../components/AppHeader'; import { RoomCard, type Room } from '../../components/chat/RoomCard'; import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet'; import { useColors } from '../../lib/theme'; type DmConversation = { partnerId: string; partnerName: string; partnerAvatar: string | null; lastMessage: string; lastMessageAt: string; unreadCount: number; isOwn: boolean; }; function formatTime(ts: string, justNowLabel: string): string { const diff = Date.now() - new Date(ts).getTime(); if (diff < 60_000) return justNowLabel; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h`; return new Date(ts).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); } function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) { const { t } = useTranslation(); const colors = useColors(); const styles = makeStyles(colors); const hasUnread = conv.unreadCount > 0; return ( {conv.partnerAvatar ? ( ) : ( {conv.partnerName.slice(0, 2).toUpperCase()} )} {conv.partnerName} {formatTime(conv.lastMessageAt, t('chat.just_now'))} {conv.isOwn ? `${t('chat.you')} ` : ''} {conv.lastMessage} {hasUnread && ( {conv.unreadCount > 99 ? '99+' : conv.unreadCount} )} ); } export default function ChatScreen() { const { t } = useTranslation(); const router = useRouter(); const colors = useColors(); const styles = makeStyles(colors); const [tab, setTab] = useState<'groups' | 'direct'>('groups'); const [createOpen, setCreateOpen] = useState(false); const { data: rooms = [], isLoading: loadingRooms, isRefetching: refetchingRooms, refetch: refetchRooms, } = useQuery({ queryKey: ['chat-rooms'], queryFn: () => apiFetch('/api/chat/rooms'), staleTime: 30_000, }); const { data: convs = [], isLoading: loadingDms, isRefetching: refetchingDms, refetch: refetchDms, } = useQuery({ queryKey: ['dm-conversations'], queryFn: () => apiFetch('/api/chat/dm-conversations'), staleTime: 30_000, enabled: tab === 'direct', }); const unreadDms = convs.reduce((s, c) => s + (c.unreadCount ?? 0), 0); const openRoom = useCallback( (roomId: string) => { router.push(`/room?roomId=${roomId}`); }, [router], ); const openDm = useCallback( (userId: string) => { router.push(`/dm?userId=${userId}`); }, [router], ); return ( {/* Header */} {t('chat.title')} {tab === 'groups' && ( setCreateOpen(true)} activeOpacity={0.7} style={styles.createBtn} > )} {/* Tabs */} setTab('groups')} activeOpacity={0.7} style={[styles.tab, tab === 'groups' && styles.tabActive]} > {t('chat.groups')} setTab('direct')} activeOpacity={0.7} style={[styles.tab, tab === 'direct' && styles.tabActive]} > {t('chat.direct')} {unreadDms > 0 && ( {unreadDms} )} {tab === 'groups' ? ( item.id} refreshControl={ } ListEmptyComponent={ loadingRooms ? ( ) : ( {t('chat.no_rooms')} ) } renderItem={({ item }) => openRoom(item.id)} />} contentContainerStyle={{ paddingBottom: 100 }} /> ) : ( item.partnerId} refreshControl={ } ListEmptyComponent={ loadingDms ? ( ) : ( {t('chat.no_chats')} ) } renderItem={({ item }) => openDm(item.partnerId)} />} contentContainerStyle={{ paddingBottom: 100 }} /> )} setCreateOpen(false)} onCreated={(room) => { refetchRooms(); openRoom(room.id); }} /> ); } function makeStyles(colors: ReturnType) { return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg }, headerSection: { paddingHorizontal: 16, paddingTop: 14, paddingBottom: 10, backgroundColor: colors.bg, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, }, titleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, title: { fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: colors.text, }, createBtn: { width: 34, height: 34, borderRadius: 17, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center', }, tabs: { flexDirection: 'row', marginTop: 12, backgroundColor: colors.surfaceElevated, borderRadius: 10, padding: 3, }, tab: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 7, borderRadius: 8, }, tabActive: { backgroundColor: colors.surface, shadowColor: '#000', shadowOpacity: 0.05, shadowRadius: 2, shadowOffset: { width: 0, height: 1 }, }, tabText: { fontSize: 12, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted, marginLeft: 5, }, tabTextActive: { color: '#007AFF', fontFamily: 'Nunito_700Bold', }, tabBadge: { minWidth: 16, height: 16, borderRadius: 8, backgroundColor: '#007AFF', paddingHorizontal: 4, alignItems: 'center', justifyContent: 'center', marginLeft: 5, }, tabBadgeText: { fontSize: 9, fontFamily: 'Nunito_700Bold', color: '#fff', }, emptyBox: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, paddingHorizontal: 32, }, emptyText: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.textMuted, marginTop: 12, }, dmRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: colors.bg, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.border, minHeight: 68, }, dmAvatar: { width: 48, height: 48, borderRadius: 24, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', marginRight: 12, flexShrink: 0, }, dmAvatarImg: { width: 48, height: 48 }, dmAvatarInitials: { fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.textMuted, }, dmInfo: { flex: 1, minWidth: 0 }, dmHeaderRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, dmName: { fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text, flexShrink: 1, marginRight: 6, }, dmTime: { fontSize: 11, fontFamily: 'Nunito_500Medium' }, dmBottomRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 2, }, dmLast: { fontSize: 12, flex: 1 }, unreadBadge: { minWidth: 20, height: 20, paddingHorizontal: 6, borderRadius: 10, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center', marginLeft: 8, }, unreadBadgeText: { fontSize: 10, fontFamily: 'Nunito_700Bold', color: '#fff', }, }); }