mobileapp / src /screens /chat /ConversationListScreen.tsx
Antaram Dev Bot
feat: complete ANTARAM.ORG ride-sharing app frontend
5c876be
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'>;
// ─── ConversationItem Component ──────────────────────────────────────────────────
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}
/>
);
}
// ─── Main Screen ─────────────────────────────────────────────────────────────────
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]);
// ── Filter conversations ───────────────────────────────────────────────────
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]);
// ── Pull to refresh ────────────────────────────────────────────────────────
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadConversations();
setRefreshing(false);
}, [loadConversations]);
// ── Navigate to chat ───────────────────────────────────────────────────────
const handlePress = useCallback(
(conversationId: string) => {
navigation.navigate('ChatDetail', { conversationId });
},
[navigation],
);
// ── Render ─────────────────────────────────────────────────────────────────
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>
);
}
// ─── Styles ──────────────────────────────────────────────────────────────────────
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,
},
});