| import React, { useState, useEffect, useCallback, useMemo } from 'react'; |
| import { View, StyleSheet, FlatList } from 'react-native'; |
| import { |
| Appbar, |
| Text, |
| Searchbar, |
| List, |
| Avatar, |
| Badge, |
| Divider, |
| IconButton, |
| ProgressBar, |
| useTheme, |
| } from 'react-native-paper'; |
| import { SafeAreaView } from 'react-native-safe-area-context'; |
| import { useNavigation } from '@react-navigation/native'; |
| import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; |
| import type { ChatStackParamList } from '../../navigation/types'; |
| import { useConversations } from '../../hooks/useConversations'; |
| import { useAuthStore } from '../../store/useAuthStore'; |
| import { formatRelativeTime } from '../../utils/dateFormat'; |
| import { customTheme } from '../../theme'; |
|
|
| type NavigationProp = NativeStackNavigationProp<ChatStackParamList, 'ConversationList'>; |
|
|
| |
|
|
| interface ConversationItemProps { |
| conversation: { |
| id: string; |
| participant1Id: string; |
| participant2Id: string; |
| lastMessage: string | null; |
| lastMessageAt: string | null; |
| unreadCount: number | null; |
| createdAt: string | null; |
| }; |
| currentUserId: string; |
| onPress: (conversationId: string) => void; |
| } |
|
|
| function ConversationItem({ conversation, currentUserId, onPress }: ConversationItemProps) { |
| const theme = useTheme(); |
| const otherId = |
| conversation.participant1Id === currentUserId |
| ? conversation.participant2Id |
| : conversation.participant1Id; |
|
|
| const otherName = `User ${otherId.slice(0, 6)}`; |
| const unread = conversation.unreadCount ?? 0; |
|
|
| return ( |
| <List.Item |
| title={otherName} |
| description={ |
| conversation.lastMessage |
| ? `${conversation.lastMessage}` |
| : 'No messages yet' |
| } |
| descriptionNumberOfLines={1} |
| left={(props) => ( |
| <Avatar.Text |
| {...props} |
| label={otherName.charAt(0).toUpperCase()} |
| size={48} |
| style={{ backgroundColor: customTheme.colors.brand.primary }} |
| /> |
| )} |
| right={(props) => ( |
| <View style={styles.conversationRight}> |
| {conversation.lastMessageAt && ( |
| <Text |
| variant="labelSmall" |
| style={{ color: theme.colors.outline }} |
| > |
| {formatRelativeTime(conversation.lastMessageAt)} |
| </Text> |
| )} |
| {unread > 0 && ( |
| <Badge style={styles.badge} size={20}> |
| {unread} |
| </Badge> |
| )} |
| </View> |
| )} |
| onPress={() => onPress(conversation.id)} |
| style={styles.conversationItem} |
| /> |
| ); |
| } |
|
|
| |
|
|
| export default function ConversationListScreen() { |
| const navigation = useNavigation<NavigationProp>(); |
| const theme = useTheme(); |
| const userId = useAuthStore((s) => s.user?.id ?? ''); |
| const { |
| conversations, |
| loadingConversations, |
| loadConversations, |
| } = useConversations(userId); |
|
|
| const [search, setSearch] = useState(''); |
| const [refreshing, setRefreshing] = useState(false); |
|
|
| useEffect(() => { |
| loadConversations(); |
| }, [loadConversations]); |
|
|
| |
|
|
| const filteredConversations = useMemo(() => { |
| if (!search.trim()) return conversations; |
| const q = search.toLowerCase(); |
| return conversations.filter( |
| (c) => |
| c.participant1Id.toLowerCase().includes(q) || |
| c.participant2Id.toLowerCase().includes(q) || |
| (c.lastMessage ?? '').toLowerCase().includes(q), |
| ); |
| }, [conversations, search]); |
|
|
| |
|
|
| const onRefresh = useCallback(async () => { |
| setRefreshing(true); |
| await loadConversations(); |
| setRefreshing(false); |
| }, [loadConversations]); |
|
|
| |
|
|
| const handlePress = useCallback( |
| (conversationId: string) => { |
| navigation.navigate('ChatDetail', { conversationId }); |
| }, |
| [navigation], |
| ); |
|
|
| |
|
|
| const renderItem = ({ item }: { item: typeof conversations[number] }) => ( |
| <ConversationItem |
| conversation={item} |
| currentUserId={userId} |
| onPress={handlePress} |
| /> |
| ); |
|
|
| if (loadingConversations && conversations.length === 0) { |
| return ( |
| <SafeAreaView style={styles.container} edges={['top']}> |
| <Appbar.Header> |
| <Appbar.Content title="Messages" /> |
| </Appbar.Header> |
| <View style={styles.center}> |
| <ProgressBar indeterminate visible /> |
| </View> |
| </SafeAreaView> |
| ); |
| } |
|
|
| return ( |
| <SafeAreaView style={styles.container} edges={['top']}> |
| <Appbar.Header> |
| <Appbar.Content title="Messages" /> |
| </Appbar.Header> |
| |
| {/* Search */} |
| <Searchbar |
| placeholder="Search conversations..." |
| value={search} |
| onChangeText={setSearch} |
| style={styles.searchbar} |
| /> |
| |
| {/* Conversations List */} |
| {filteredConversations.length === 0 ? ( |
| <View style={styles.emptyContainer}> |
| <IconButton |
| icon="chat-outline" |
| size={64} |
| iconColor={theme.colors.outlineVariant} |
| /> |
| <Text variant="bodyLarge" style={styles.emptyTitle}> |
| No conversations yet |
| </Text> |
| <Text variant="bodySmall" style={styles.emptySubtext}> |
| Your conversations will appear here |
| </Text> |
| </View> |
| ) : ( |
| <FlatList |
| data={filteredConversations} |
| keyExtractor={(item) => item.id} |
| renderItem={renderItem} |
| refreshing={refreshing} |
| onRefresh={onRefresh} |
| ItemSeparatorComponent={Divider} |
| contentContainerStyle={styles.listContent} |
| /> |
| )} |
| </SafeAreaView> |
| ); |
| } |
|
|
| |
|
|
| const styles = StyleSheet.create({ |
| container: { |
| flex: 1, |
| backgroundColor: customTheme.colors.brand.background, |
| }, |
| center: { |
| flex: 1, |
| justifyContent: 'center', |
| alignItems: 'center', |
| }, |
| searchbar: { |
| marginHorizontal: 16, |
| marginBottom: 8, |
| borderRadius: 28, |
| }, |
| listContent: { |
| paddingBottom: 24, |
| }, |
| conversationItem: { |
| paddingHorizontal: 4, |
| }, |
| conversationRight: { |
| justifyContent: 'center', |
| alignItems: 'flex-end', |
| gap: 4, |
| }, |
| badge: { |
| backgroundColor: customTheme.colors.brand.error, |
| }, |
| emptyContainer: { |
| flex: 1, |
| justifyContent: 'center', |
| alignItems: 'center', |
| paddingHorizontal: 32, |
| }, |
| emptyTitle: { |
| marginTop: 8, |
| fontWeight: '600', |
| }, |
| emptySubtext: { |
| marginTop: 4, |
| textAlign: 'center', |
| opacity: 0.6, |
| }, |
| }); |
|
|