Initial commit
This commit is contained in:
278
app/src/screens/ChatScreen/ChatDrawer.tsx
Normal file
278
app/src/screens/ChatScreen/ChatDrawer.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
Dimensions,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
import type { Conversation } from '../../store/chatTypes';
|
||||
import { colors, spacing, typography, borderRadius } from '../../theme/lightTheme';
|
||||
|
||||
const DRAWER_WIDTH = Math.min(Dimensions.get('window').width * 0.80, 310);
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
conversations: Conversation[];
|
||||
activeConversationId: string | null;
|
||||
onClose: () => void;
|
||||
onNewChat: () => void;
|
||||
onSelectConversation: (id: string) => void;
|
||||
onDeleteConversation: (id: string) => void;
|
||||
}
|
||||
|
||||
function formatDate(ts: number): string {
|
||||
const diffDays = Math.floor((Date.now() - ts) / 86400000);
|
||||
if (diffDays === 0) return "Aujourd'hui";
|
||||
if (diffDays === 1) return 'Hier';
|
||||
if (diffDays < 7) return `Il y a ${diffDays} j`;
|
||||
return new Date(ts).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
|
||||
}
|
||||
|
||||
export default function ChatDrawer({
|
||||
visible,
|
||||
conversations,
|
||||
activeConversationId,
|
||||
onClose,
|
||||
onNewChat,
|
||||
onSelectConversation,
|
||||
onDeleteConversation,
|
||||
}: Props) {
|
||||
const translateX = useRef(new Animated.Value(-DRAWER_WIDTH)).current;
|
||||
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
||||
// Keep mounted during the close animation
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setMounted(true);
|
||||
Animated.parallel([
|
||||
Animated.timing(translateX, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backdropOpacity, {
|
||||
toValue: 0.5,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(translateX, {
|
||||
toValue: -DRAWER_WIDTH,
|
||||
duration: 210,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backdropOpacity, {
|
||||
toValue: 0,
|
||||
duration: 210,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => setMounted(false));
|
||||
}
|
||||
}, [visible, translateX, backdropOpacity]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const sorted = [...conversations].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
|
||||
const renderItem = ({ item }: { item: Conversation }) => {
|
||||
const isActive = item.id === activeConversationId;
|
||||
const lastMsg = item.messages[item.messages.length - 1];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.convItem, isActive && styles.convItemActive]}
|
||||
onPress={() => { onSelectConversation(item.id); onClose(); }}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
<View style={styles.convItemContent}>
|
||||
<Text
|
||||
style={[styles.convTitle, isActive && styles.convTitleActive]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text style={styles.convMeta}>
|
||||
{item.messages.length} msg · {formatDate(item.updatedAt)}
|
||||
</Text>
|
||||
{lastMsg ? (
|
||||
<Text style={styles.convPreview} numberOfLines={1}>
|
||||
{lastMsg.role === 'user' ? '👤 ' : '🤖 '}
|
||||
{lastMsg.content}
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.convPreviewEmpty}>Vide</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.deleteBtn}
|
||||
onPress={() => onDeleteConversation(item.id)}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Text style={styles.deleteBtnText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFillObject} pointerEvents="box-none">
|
||||
{/* Backdrop */}
|
||||
<Animated.View
|
||||
style={[styles.backdrop, { opacity: backdropOpacity }]}
|
||||
pointerEvents={visible ? 'auto' : 'none'}
|
||||
>
|
||||
<Pressable style={StyleSheet.absoluteFillObject} onPress={onClose} />
|
||||
</Animated.View>
|
||||
|
||||
{/* Drawer panel */}
|
||||
<Animated.View style={[styles.drawer, { transform: [{ translateX }] }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.drawerHeader}>
|
||||
<Text style={styles.drawerTitle}>Conversations</Text>
|
||||
<TouchableOpacity
|
||||
onPress={onClose}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Text style={styles.closeBtnText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* New chat button */}
|
||||
<TouchableOpacity
|
||||
style={styles.newChatBtn}
|
||||
onPress={() => { onNewChat(); onClose(); }}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.newChatBtnText}>+ Nouveau chat</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Conversations list */}
|
||||
<FlatList
|
||||
data={sorted}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListEmptyComponent={
|
||||
<Text style={styles.emptyText}>Aucune conversation</Text>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: colors.textPrimary,
|
||||
},
|
||||
drawer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: DRAWER_WIDTH,
|
||||
backgroundColor: colors.surface,
|
||||
shadowColor: colors.textPrimary,
|
||||
shadowOffset: { width: 4, height: 0 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
elevation: 16,
|
||||
},
|
||||
drawerHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.xxl,
|
||||
paddingBottom: spacing.md,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
drawerTitle: {
|
||||
fontSize: typography.sizes.lg,
|
||||
fontWeight: typography.weights.semibold,
|
||||
color: colors.textPrimary,
|
||||
},
|
||||
closeBtnText: {
|
||||
fontSize: 18,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
newChatBtn: {
|
||||
margin: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
},
|
||||
newChatBtnText: {
|
||||
color: colors.surface,
|
||||
fontSize: typography.sizes.md,
|
||||
fontWeight: typography.weights.semibold,
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
convItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
marginBottom: 2,
|
||||
},
|
||||
convItemActive: {
|
||||
backgroundColor: colors.surfaceSecondary,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary,
|
||||
},
|
||||
convItemContent: {
|
||||
flex: 1,
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
convTitle: {
|
||||
fontSize: typography.sizes.sm,
|
||||
fontWeight: typography.weights.medium,
|
||||
color: colors.textPrimary,
|
||||
marginBottom: 2,
|
||||
},
|
||||
convTitleActive: {
|
||||
color: colors.primary,
|
||||
fontWeight: typography.weights.semibold,
|
||||
},
|
||||
convMeta: {
|
||||
fontSize: 11,
|
||||
color: colors.textTertiary,
|
||||
marginBottom: 2,
|
||||
},
|
||||
convPreview: {
|
||||
fontSize: 12,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
convPreviewEmpty: {
|
||||
fontSize: 12,
|
||||
color: colors.textTertiary,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
deleteBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
deleteBtnText: {
|
||||
fontSize: 14,
|
||||
color: colors.textTertiary,
|
||||
},
|
||||
emptyText: {
|
||||
textAlign: 'center',
|
||||
color: colors.textTertiary,
|
||||
marginTop: spacing.xl,
|
||||
fontSize: typography.sizes.sm,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user