Files
MyMobileAgent/app/src/screens/ChatScreen/ChatDrawer.tsx
Jonathan Atta da373199e0 Initial commit
2026-03-03 10:33:56 +01:00

279 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
});