mobileapp / src /screens /chat /ChatDetailScreen.tsx
Antaram Dev Bot
feat: complete ANTARAM.ORG ride-sharing app frontend
5c876be
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform } from 'react-native';
import {
Appbar,
Text,
Surface,
IconButton,
Avatar,
Menu,
Divider,
TextInput,
ProgressBar,
useTheme,
} from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRoute, useNavigation } from '@react-navigation/native';
import type { RouteProp } 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 { formatChatTimestamp } from '../../utils/dateFormat';
import { customTheme } from '../../theme';
type RouteProps = RouteProp<ChatStackParamList, 'ChatDetail'>;
type NavigationProp = NativeStackNavigationProp<ChatStackParamList, 'ChatDetail'>;
// ─── MessageBubble Component ─────────────────────────────────────────────────────
interface MessageBubbleProps {
message: {
id: string;
conversationId: string;
senderId: string;
content: string;
type: string;
status: string;
createdAt: string | null;
};
currentUserId: string;
otherName: string;
}
function MessageBubble({ message, currentUserId, otherName }: MessageBubbleProps) {
const theme = useTheme();
const isMe = message.senderId === currentUserId;
const isSystem = message.type === 'system';
if (isSystem) {
return (
<View style={styles.systemMessageContainer}>
<Text variant="labelSmall" style={{ color: theme.colors.outline }}>
{message.content}
</Text>
</View>
);
}
return (
<View
style={[
styles.bubbleRow,
isMe ? styles.bubbleRowRight : styles.bubbleRowLeft,
]}
>
{!isMe && (
<Avatar.Text
size={32}
label={otherName.charAt(0).toUpperCase()}
style={[styles.bubbleAvatar, { backgroundColor: customTheme.colors.brand.primary }]}
/>
)}
<Surface
style={[
styles.bubble,
isMe
? { backgroundColor: customTheme.colors.brand.primary }
: { backgroundColor: theme.colors.surfaceVariant },
]}
elevation={0}
>
<Text
variant="bodyMedium"
style={[
styles.bubbleText,
isMe && { color: '#FFFFFF' },
]}
>
{message.content}
</Text>
<View style={[styles.bubbleFooter, isMe && styles.bubbleFooterRight]}>
<Text
variant="labelSmall"
style={[
styles.bubbleTime,
isMe && { color: 'rgba(255,255,255,0.7)' },
]}
>
{message.createdAt ? formatChatTimestamp(message.createdAt) : ''}
</Text>
{isMe && (
<Text
variant="labelSmall"
style={{ color: 'rgba(255,255,255,0.7)' }}
>
{message.status === 'read' ? '\u2713\u2713' : message.status === 'delivered' ? '\u2713\u2713' : '\u2713'}
</Text>
)}
</View>
</Surface>
</View>
);
}
// ─── ChatInput Component ─────────────────────────────────────────────────────────
interface ChatInputProps {
onSend: (text: string) => void;
}
function ChatInput({ onSend }: ChatInputProps) {
const [text, setText] = useState('');
const handleSend = useCallback(() => {
const trimmed = text.trim();
if (trimmed) {
onSend(trimmed);
setText('');
}
}, [text, onSend]);
return (
<Surface style={styles.inputContainer} elevation={2}>
<TextInput
mode="flat"
placeholder="Type a message..."
value={text}
onChangeText={setText}
multiline
maxLength={1000}
style={styles.input}
contentStyle={styles.inputContent}
underlineColor="transparent"
activeUnderlineColor="transparent"
/>
<IconButton
icon="send"
size={24}
onPress={handleSend}
disabled={!text.trim()}
iconColor={
text.trim()
? customTheme.colors.brand.primary
: customTheme.colors.brand.surface
}
style={styles.sendButton}
/>
</Surface>
);
}
// ─── Main Screen ─────────────────────────────────────────────────────────────────
export default function ChatDetailScreen() {
const route = useRoute<RouteProps>();
const navigation = useNavigation<NavigationProp>();
const theme = useTheme();
const { conversationId } = route.params;
const userId = useAuthStore((s) => s.user?.id ?? '');
const {
messages,
loadingMessages,
loadMessages,
sendMessage,
markAsRead,
} = useConversations(userId);
const [menuVisible, setMenuVisible] = useState(false);
const flatListRef = useRef<FlatList>(null);
// ── Load messages on mount ─────────────────────────────────────────────────
useEffect(() => {
loadMessages(conversationId);
markAsRead(conversationId);
}, [conversationId, loadMessages, markAsRead]);
// ── Auto-scroll to bottom on new messages ──────────────────────────────────
useEffect(() => {
if (messages.length > 0) {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
}
}, [messages.length]);
// ── Send message handler ───────────────────────────────────────────────────
const handleSend = useCallback(
async (text: string) => {
await sendMessage(conversationId, userId, text, 'text');
},
[conversationId, userId, sendMessage],
);
// ── Menu handlers ──────────────────────────────────────────────────────────
const otherId = `User`;
// ── Render ─────────────────────────────────────────────────────────────────
const renderItem = ({ item }: { item: typeof messages[number] }) => (
<MessageBubble message={item} currentUserId={userId} otherName={otherId} />
);
return (
<SafeAreaView style={styles.container} edges={['top']} mode="padding">
{/* Appbar */}
<Appbar.Header>
<Appbar.BackAction onPress={navigation.goBack} />
<Avatar.Text
size={32}
label={otherId.charAt(0)}
style={{ backgroundColor: customTheme.colors.brand.primary, marginRight: 8 }}
/>
<Appbar.Content title={otherId} subtitle="Online" />
<Appbar.Action icon="phone-outline" onPress={() => {}} />
<Menu
visible={menuVisible}
onDismiss={() => setMenuVisible(false)}
anchor={
<Appbar.Action
icon="dots-vertical"
onPress={() => setMenuVisible(true)}
/>
}
>
<Menu.Item onPress={() => { setMenuVisible(false); }} title="View Profile" leadingIcon="account" />
<Menu.Item onPress={() => { setMenuVisible(false); }} title="Share Location" leadingIcon="map-marker" />
<Menu.Item onPress={() => { setMenuVisible(false); }} title="Block" leadingIcon="block-helper" />
</Menu>
</Appbar.Header>
{/* Messages */}
{loadingMessages && messages.length === 0 ? (
<View style={styles.center}>
<ProgressBar indeterminate visible />
</View>
) : (
<FlatList
ref={flatListRef}
data={messages}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={styles.messagesList}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
/>
)}
{/* Input */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={0}
>
<ChatInput onSend={handleSend} />
</KeyboardAvoidingView>
</SafeAreaView>
);
}
// ─── Styles ──────────────────────────────────────────────────────────────────────
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: customTheme.colors.brand.background,
},
center: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
messagesList: {
paddingHorizontal: 12,
paddingVertical: 8,
paddingBottom: 8,
},
bubbleRow: {
flexDirection: 'row',
alignItems: 'flex-end',
marginBottom: 8,
maxWidth: '80%',
},
bubbleRowLeft: {
alignSelf: 'flex-start',
},
bubbleRowRight: {
alignSelf: 'flex-end',
},
bubbleAvatar: {
marginRight: 8,
marginBottom: 4,
},
bubble: {
borderRadius: 16,
paddingHorizontal: 14,
paddingVertical: 10,
maxWidth: '100%',
},
bubbleText: {
lineHeight: 20,
},
bubbleFooter: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
gap: 4,
marginTop: 4,
},
bubbleFooterRight: {
justifyContent: 'flex-end',
},
bubbleTime: {
fontSize: 10,
opacity: 0.7,
},
systemMessageContainer: {
alignSelf: 'center',
marginVertical: 8,
paddingHorizontal: 12,
paddingVertical: 4,
backgroundColor: customTheme.colors.brand.surface,
borderRadius: 12,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 8,
paddingVertical: 4,
backgroundColor: customTheme.colors.brand.surface,
borderTopColor: customTheme.colors.brand.primaryLight,
borderTopWidth: StyleSheet.hairlineWidth,
},
input: {
flex: 1,
backgroundColor: 'transparent',
maxHeight: 100,
fontSize: 15,
},
inputContent: {
paddingBottom: 8,
},
sendButton: {
margin: 0,
},
});