feat(mobile): real chat list with pull-to-refresh + logout
This commit is contained in:
+121
-15
@@ -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<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 (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Netralax</Text>
|
||||
<Text style={styles.subtitle}>Chats — coming soon (Phase 1)</Text>
|
||||
<Stack.Screen
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user