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
|
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' },
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user