diff --git a/apps/mobile/app/(app)/chats.tsx b/apps/mobile/app/(app)/chats.tsx index 929e99d..528d2fa 100644 --- a/apps/mobile/app/(app)/chats.tsx +++ b/apps/mobile/app/(app)/chats.tsx @@ -1,25 +1,131 @@ -import { StyleSheet, Text, View } from 'react-native'; +import { chat } from '@chat-app/shared'; +import type { ConversationSummary } from '@chat-app/shared/chat'; +import { Stack, useRouter } from 'expo-router'; +import { useCallback, useEffect, useState } from 'react'; +import { + ActivityIndicator, + Alert, + FlatList, + Pressable, + RefreshControl, + StyleSheet, + Text, + View, +} from 'react-native'; -// Phase 0 stand-in for the conversation list. Confirms the authenticated -// route group (`(app)`) mounts without error after a navigation from -// Landing. Phase 1 replaces this with the real chat list. +import { ConversationRow } from '../../components/ConversationRow'; +import { useAuth } from '../../lib/authContext'; +import { supabase } from '../../lib/supabase'; +import { colors } from '../../theme/colors'; + +// Phase 1 conversation list. The last-message preview is the static +// fallback ('…') — fetching + decrypting last-messages per conversation +// is a Phase 1.5 follow-up. Tap → conversation detail. export default function Chats() { + const router = useRouter(); + const { signOut } = useAuth(); + const [list, setList] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + + const load = useCallback(async () => { + setError(null); + try { + const result = await chat.listConversations(supabase); + setList(result.filter((c) => !c.archived)); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Konversationen konnten nicht geladen werden'); + } + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const onRefresh = useCallback(async () => { + setRefreshing(true); + await load(); + setRefreshing(false); + }, [load]); + + const confirmLogout = () => { + Alert.alert('Abmelden', 'Diese Sitzung beenden?', [ + { text: 'Abbrechen', style: 'cancel' }, + { + text: 'Abmelden', + style: 'destructive', + onPress: () => { + void signOut(); + }, + }, + ]); + }; + return ( - Netralax - Chats — coming soon (Phase 1) + ( + + Abmelden + + ), + }} + /> + + {list === null && !error && ( + + + + )} + + {error && {error}} + + {list && list.length === 0 && !error && ( + + Noch keine Konversationen. + + Lege dir auf dem Desktop einen Chat an — er taucht hier auf, sobald du + herunterziehst, um zu aktualisieren. + + + )} + + {list && list.length > 0 && ( + item.id} + renderItem={({ item }) => ( + router.push('/(app)/conversations/' + item.id)} + /> + )} + refreshControl={ + + } + ItemSeparatorComponent={() => } + /> + )} ); } const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: '#0b0b0f', - padding: 24, - }, - title: { color: '#fff', fontSize: 28, fontWeight: '600' }, - subtitle: { color: '#9ca3af', marginTop: 8, textAlign: 'center' }, + container: { flex: 1, backgroundColor: colors.bg }, + loading: { flex: 1, alignItems: 'center', justifyContent: 'center' }, + empty: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, gap: 8 }, + emptyText: { color: colors.text, fontSize: 16, fontWeight: '600' }, + emptyHint: { color: colors.textMuted, fontSize: 13, textAlign: 'center', lineHeight: 18 }, + error: { color: colors.danger, padding: 16, textAlign: 'center' }, + separator: { height: 1, backgroundColor: colors.border, marginLeft: 68 }, + logoutLink: { color: colors.accent, fontSize: 14, fontWeight: '600', paddingHorizontal: 8 }, });