feat(mobile): real chat list with pull-to-refresh + logout

This commit is contained in:
byGalax
2026-05-14 00:00:24 +02:00
parent 0b3a6280fc
commit 3755a927c3
+121 -15
View File
@@ -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 import { ConversationRow } from '../../components/ConversationRow';
// route group (`(app)`) mounts without error after a navigation from import { useAuth } from '../../lib/authContext';
// Landing. Phase 1 replaces this with the real chat list. 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() { export default function Chats() {
const router = useRouter();
const { signOut } = useAuth();
const [list, setList] = useState<ConversationSummary[] | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(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 ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.title}>Netralax</Text> <Stack.Screen
<Text style={styles.subtitle}>Chats coming soon (Phase 1)</Text> options={{
title: 'Chats',
headerStyle: { backgroundColor: colors.bg },
headerTitleStyle: { color: colors.text },
headerRight: () => (
<Pressable onPress={confirmLogout} hitSlop={10}>
<Text style={styles.logoutLink}>Abmelden</Text>
</Pressable>
),
}}
/>
{list === null && !error && (
<View style={styles.loading}>
<ActivityIndicator color={colors.accent} />
</View>
)}
{error && <Text style={styles.error}>{error}</Text>}
{list && list.length === 0 && !error && (
<View style={styles.empty}>
<Text style={styles.emptyText}>Noch keine Konversationen.</Text>
<Text style={styles.emptyHint}>
Lege dir auf dem Desktop einen Chat an er taucht hier auf, sobald du
herunterziehst, um zu aktualisieren.
</Text>
</View>
)}
{list && list.length > 0 && (
<FlatList
data={list}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ConversationRow
conversation={item}
lastMessagePreview="…"
onPress={() => router.push('/(app)/conversations/' + item.id)}
/>
)}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={colors.accent}
/>
}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
)}
</View> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: { flex: 1, backgroundColor: colors.bg },
flex: 1, loading: { flex: 1, alignItems: 'center', justifyContent: 'center' },
alignItems: 'center', empty: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, gap: 8 },
justifyContent: 'center', emptyText: { color: colors.text, fontSize: 16, fontWeight: '600' },
backgroundColor: '#0b0b0f', emptyHint: { color: colors.textMuted, fontSize: 13, textAlign: 'center', lineHeight: 18 },
padding: 24, error: { color: colors.danger, padding: 16, textAlign: 'center' },
}, separator: { height: 1, backgroundColor: colors.border, marginLeft: 68 },
title: { color: '#fff', fontSize: 28, fontWeight: '600' }, logoutLink: { color: colors.accent, fontSize: 14, fontWeight: '600', paddingHorizontal: 8 },
subtitle: { color: '#9ca3af', marginTop: 8, textAlign: 'center' },
}); });