import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, FlatList, KeyboardAvoidingView, Modal, Platform, Pressable, RefreshControl, StyleSheet, Text, TextInput, View, } from 'react-native'; import { ChatMember, ChatMessage, ChatRoom, createChatRoom, fetchChatMembers, fetchChatMessages, fetchChatRooms, markChatRoomRead, provisionChatRoom, sendChatMessage, syncChatRoom, } from '@/src/lib/api'; import { useAuth } from '@/src/providers/auth-provider'; const PRIMARY = '#69c350'; function normalizeRoomKey(value: string): string { return value.trim().toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/ß/g, 'ss').replace(/[^a-z0-9._=-]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48); } function messageTime(value: ChatMessage['timestamp']): string { if (!value) return ''; return new Date(value).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }); } function mergeMessages(current: ChatMessage[], incoming: ChatMessage[]): ChatMessage[] { const byId = new Map(); for (const message of current) byId.set(message.id, message); for (const message of incoming) byId.set(message.id, message); return Array.from(byId.values()).sort((a, b) => a.id - b.id); } export default function CommunicationScreen() { const { token } = useAuth(); const listRef = useRef>(null); const [rooms, setRooms] = useState([]); const [members, setMembers] = useState([]); const [messages, setMessages] = useState([]); const [activeRoomKey, setActiveRoomKey] = useState('allgemein'); const [draft, setDraft] = useState(''); const [search, setSearch] = useState(''); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const [membersOpen, setMembersOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [roomName, setRoomName] = useState(''); const [roomTopic, setRoomTopic] = useState(''); const [roomKey, setRoomKey] = useState(''); const afterIdRef = useRef(0); const activeRoom = useMemo( () => rooms.find((room) => room.key === activeRoomKey) || rooms[0] || null, [activeRoomKey, rooms] ); const visibleRooms = useMemo(() => { const query = search.trim().toLowerCase(); return rooms.filter((room) => !query || [room.name, room.topic, room.email, room.projectNumber] .filter(Boolean).some((value) => String(value).toLowerCase().includes(query))); }, [rooms, search]); const loadRooms = useCallback(async () => { if (!token) return []; const result = await fetchChatRooms(token); setRooms(result); return result; }, [token]); const loadRoom = useCallback(async (key: string) => { if (!token) return; const [nextMessages, nextMembers] = await Promise.all([ fetchChatMessages(token, key), fetchChatMembers(token, key), ]); setMessages(nextMessages); setMembers(nextMembers); afterIdRef.current = nextMessages[nextMessages.length - 1]?.id || 0; await markChatRoomRead(token, key, afterIdRef.current); }, [token]); const refresh = useCallback(async () => { if (!token) return; try { setError(null); const nextRooms = await loadRooms(); const selected = nextRooms.find((room) => room.key === activeRoomKey) || nextRooms[0]; if (selected?.exists) { setActiveRoomKey(selected.key); await loadRoom(selected.key); } } catch (nextError) { setError(nextError instanceof Error ? nextError.message : 'Chat konnte nicht geladen werden.'); } finally { setLoading(false); setRefreshing(false); } }, [activeRoomKey, loadRoom, loadRooms, token]); useEffect(() => { void refresh(); }, [refresh]); useEffect(() => { if (!token || !activeRoom?.exists) return undefined; const id = setInterval(async () => { try { const result = await syncChatRoom(token, activeRoom.key, afterIdRef.current); if (!result.messages?.length) return; setMessages((current) => mergeMessages(current, result.messages || [])); afterIdRef.current = result.nextId || afterIdRef.current; await markChatRoomRead(token, activeRoom.key, afterIdRef.current); } catch { // Beim nächsten Intervall erneut versuchen. } }, 3000); return () => clearInterval(id); }, [activeRoom, token]); async function selectRoom(room: ChatRoom) { if (!token) return; try { setLoading(true); const readyRoom = room.exists ? room : await provisionChatRoom(token, room); setActiveRoomKey(readyRoom.key); await loadRooms(); await loadRoom(readyRoom.key); } catch (nextError) { setError(nextError instanceof Error ? nextError.message : 'Chatraum konnte nicht geöffnet werden.'); } finally { setLoading(false); } } async function submitMessage() { if (!token || !activeRoom?.exists || !draft.trim() || sending) return; const text = draft.trim(); setDraft(''); setSending(true); try { const message = await sendChatMessage(token, activeRoom.key, text); setMessages((current) => mergeMessages(current, [message])); afterIdRef.current = Math.max(afterIdRef.current, message.id); await markChatRoomRead(token, activeRoom.key, message.id); } catch (nextError) { setDraft(text); setError(nextError instanceof Error ? nextError.message : 'Nachricht konnte nicht gesendet werden.'); } finally { setSending(false); } } async function submitRoom() { if (!token || !roomName.trim()) return; try { const created = await createChatRoom(token, { key: normalizeRoomKey(roomKey || roomName), name: roomName.trim(), topic: roomTopic.trim() || null, }); setCreateOpen(false); setRoomName(''); setRoomKey(''); setRoomTopic(''); await loadRooms(); await selectRoom(created); } catch (nextError) { setError(nextError instanceof Error ? nextError.message : 'Chatraum konnte nicht erstellt werden.'); } } function renderRoom({ item }: { item: ChatRoom }) { const active = item.key === activeRoom?.key; return ( void selectRoom(item)}> {item.group || 'Räume'} {item.name} {item.unread ? {item.unread} : null} ); } function renderMessage({ item }: { item: ChatMessage }) { return ( {!item.own ? {item.senderDisplayName || 'Benutzer'} : null} {item.body} {messageTime(item.timestamp)} ); } return ( Chat {activeRoom?.name || 'FEDEO Kommunikation'} setCreateOpen(true)}>+ setMembersOpen((value) => !value)}> {error ? {error} : null} item.key} renderItem={renderRoom} showsHorizontalScrollIndicator={false} contentContainerStyle={styles.roomList} /> {membersOpen ? ( Teilnehmer · {members.length} {members.map((member) => member.own ? 'Du' : member.displayName).join(' · ')} ) : null} {loading ? ( Nachrichten werden geladen … ) : ( String(item.id)} renderItem={renderMessage} contentContainerStyle={styles.messageList} refreshControl={ { setRefreshing(true); void refresh(); }} />} ListEmptyComponent={Noch keine Nachrichten in diesem Raum.} onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: true })} /> )} void submitMessage()}> {sending ? '…' : 'Senden'} setCreateOpen(false)}> Chatraum erstellen setCreateOpen(false)}>Abbrechen void submitRoom()}>Erstellen ); } const styles = StyleSheet.create({ screen: { flex: 1, backgroundColor: '#f9fafb' }, header: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 16, backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: '#e5e7eb' }, headerMain: { flex: 1 }, title: { fontSize: 20, fontWeight: '800', color: '#111827' }, subtitle: { color: '#6b7280', marginTop: 2 }, headerButton: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: '#d1d5db' }, headerButtonText: { fontSize: 20, fontWeight: '700' }, error: { margin: 10, padding: 10, borderRadius: 8, color: '#991b1b', backgroundColor: '#fee2e2' }, roomPanel: { paddingVertical: 10, backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: '#e5e7eb' }, searchInput: { marginHorizontal: 16, padding: 9, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 10 }, roomList: { paddingHorizontal: 12, paddingTop: 10, gap: 8 }, roomChip: { width: 150, padding: 10, borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 10 }, roomChipActive: { backgroundColor: '#eff9ea', borderColor: PRIMARY }, roomGroup: { color: '#6b7280', fontSize: 10, textTransform: 'uppercase' }, roomName: { color: '#111827', fontSize: 14, fontWeight: '700', marginTop: 2 }, roomTextActive: { color: '#2f6f25' }, roomBadge: { position: 'absolute', right: 6, top: 6, backgroundColor: PRIMARY, color: '#fff', borderRadius: 9, paddingHorizontal: 6, overflow: 'hidden', fontSize: 11 }, membersPanel: { padding: 12, backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: '#e5e7eb' }, membersTitle: { fontWeight: '700', color: '#111827' }, membersText: { marginTop: 4, color: '#6b7280', fontSize: 12 }, loading: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8 }, loadingText: { color: '#6b7280' }, messageList: { flexGrow: 1, padding: 14, gap: 10 }, emptyText: { marginTop: 40, textAlign: 'center', color: '#9ca3af' }, messageRow: { flexDirection: 'row' }, messageRowOwn: { justifyContent: 'flex-end' }, messageBubble: { maxWidth: '82%', padding: 11, borderRadius: 16, backgroundColor: '#f3f4f6' }, messageBubbleOwn: { backgroundColor: PRIMARY }, messageAuthor: { color: '#2f6f25', fontSize: 12, fontWeight: '700', marginBottom: 3 }, messageBody: { color: '#111827', fontSize: 15 }, messageBodyOwn: { color: '#fff' }, messageTime: { marginTop: 4, color: '#9ca3af', fontSize: 10, textAlign: 'right' }, messageTimeOwn: { color: '#e7f7e2' }, composer: { flexDirection: 'row', alignItems: 'flex-end', gap: 8, padding: 12, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: '#e5e7eb' }, composerInput: { flex: 1, maxHeight: 120, borderWidth: 1, borderColor: '#d1d5db', borderRadius: 18, paddingHorizontal: 13, paddingVertical: 9 }, sendButton: { backgroundColor: PRIMARY, borderRadius: 18, paddingHorizontal: 15, paddingVertical: 10 }, sendButtonDisabled: { opacity: 0.45 }, sendButtonText: { color: '#fff', fontWeight: '700' }, modalBackdrop: { flex: 1, justifyContent: 'center', padding: 24, backgroundColor: 'rgba(0,0,0,0.45)' }, modalCard: { padding: 18, gap: 12, borderRadius: 14, backgroundColor: '#fff' }, modalTitle: { fontSize: 18, fontWeight: '800', color: '#111827' }, modalInput: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 9, padding: 10 }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', gap: 18 }, linkText: { color: '#2f6f25', fontWeight: '700' }, primaryButton: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 9, backgroundColor: PRIMARY }, primaryButtonText: { color: '#fff', fontWeight: '700' }, });