import { chat } from '@chat-app/shared'; import type { ConversationSummary, DecryptedMessage } from '@chat-app/shared/chat'; import { Stack, useLocalSearchParams } from 'expo-router'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, FlatList, KeyboardAvoidingView, Platform, Pressable, StyleSheet, Text, TextInput, View, } from 'react-native'; import { useAuth } from '../../../lib/authContext'; import { supabase } from '../../../lib/supabase'; import { colors } from '../../../theme/colors'; // Phase 1 conversation detail: fetch + decrypt last 50 messages, render // them newest-at-bottom, and let the user send a text message. No // realtime subscription, no attachments, no edit/delete — those are // later phases. Pull-to-refresh re-fetches. export default function ConversationDetail() { const { id } = useLocalSearchParams<{ id: string }>(); const { user, device, ownPrivateKey } = useAuth(); const [conversation, setConversation] = useState(null); const [messages, setMessages] = useState(null); const [text, setText] = useState(''); const [sending, setSending] = useState(false); const [error, setError] = useState(null); const listRef = useRef>(null); const load = useCallback(async () => { if (!id || !device || !ownPrivateKey) return; setError(null); try { const all = await chat.listConversations(supabase); const conv = all.find((c) => c.id === id) ?? null; setConversation(conv); const ciphers = await chat.fetchConversationMessages(supabase, id, 50); const decrypted = await chat.decryptMessages({ client: supabase, messages: ciphers, ownDeviceId: device.id, ownPrivateKey, }); setMessages(decrypted); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Nachrichten konnten nicht geladen werden'); } }, [id, device, ownPrivateKey]); useEffect(() => { void load(); }, [load]); const members = useMemo(() => conversation?.members ?? [], [conversation]); const senderName = useCallback( (senderId: string) => { if (senderId === user?.id) return 'Du'; const m = members.find((mm) => mm.userId === senderId); return m?.profile?.displayName ?? m?.profile?.username ?? 'Unbekannt'; }, [members, user], ); const title = conversation?.type === 'group' ? (conversation.name ?? 'Gruppe') : (conversation?.peer?.displayName ?? '…'); async function handleSend() { if (!text.trim() || !user || !device || !ownPrivateKey || !id || sending) return; setSending(true); setError(null); try { await chat.sendEncryptedMessage({ client: supabase, conversationId: id, plaintext: text.trim(), senderUserId: user.id, senderDeviceId: device.id, senderPrivateKey: ownPrivateKey, }); setText(''); await load(); } catch (err: unknown) { setError(err instanceof Error ? err.message : 'Senden fehlgeschlagen'); } finally { setSending(false); } } return ( {messages === null && !error && ( )} {error && {error}} {messages && ( m.id} contentContainerStyle={styles.listContent} renderItem={({ item }) => ( )} onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })} /> )} {sending ? ( ) : ( Senden )} ); } function MessageRow({ senderName, mine, body, time, }: { senderName: string; mine: boolean; body: string; time: string; }) { return ( {!mine && {senderName}} {body} {time} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.bg }, loading: { flex: 1, alignItems: 'center', justifyContent: 'center' }, error: { color: colors.danger, padding: 12, textAlign: 'center' }, listContent: { padding: 12, gap: 6 }, bubbleWrap: { flexDirection: 'row', marginVertical: 2 }, bubbleWrapMine: { justifyContent: 'flex-end' }, bubbleWrapOther: { justifyContent: 'flex-start' }, bubble: { maxWidth: '78%', padding: 10, borderRadius: 14 }, bubbleMine: { backgroundColor: colors.accent }, bubbleOther: { backgroundColor: colors.surface, borderColor: colors.border, borderWidth: 1 }, sender: { color: colors.textMuted, fontSize: 11, fontWeight: '600', marginBottom: 4 }, body: { color: colors.text, fontSize: 15, lineHeight: 20 }, time: { color: colors.textDim, fontSize: 10, marginTop: 4, textAlign: 'right' }, inputRow: { flexDirection: 'row', alignItems: 'flex-end', padding: 8, gap: 8, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.surface, }, input: { flex: 1, minHeight: 40, maxHeight: 120, color: colors.text, backgroundColor: colors.bg, borderColor: colors.border, borderWidth: 1, borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 15, }, sendBtn: { backgroundColor: colors.accent, borderRadius: 10, paddingHorizontal: 16, height: 40, alignItems: 'center', justifyContent: 'center', }, sendBtnDisabled: { opacity: 0.5 }, sendBtnText: { color: colors.text, fontWeight: '600' }, });