374 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
FlatList,
Image,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
Alert,
Modal,
} from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { Header } from '@components';
import { useAppTheme } from '@theme';
import { useCustomerSupport } from '@hooks';
import { getStyles } from './supportChatScreen.styles';
import { AppStackParamList } from '../../../navigation/appStack';
import { formatTime, getFullUrl } from '../../../utils/helper';
type SupportChatRouteProp = RouteProp<AppStackParamList, 'SupportChatScreen'>;
type SupportChatNavProp = StackNavigationProp<AppStackParamList>;
// Mock attachments that users can select to simulate image attachments
const MOCK_ATTACHMENTS = [
{
id: 'att1',
label: 'Receipt Invoice',
url: 'https://images.unsplash.com/photo-1554415707-6e8cfc93fe23?w=500&auto=format&fit=crop&q=60',
},
{
id: 'att2',
label: 'Damaged Packing',
url: 'https://images.unsplash.com/photo-1607344645866-009c320c5ab8?w=500&auto=format&fit=crop&q=60',
},
{
id: 'att3',
label: 'Wrong Item Delivered',
url: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=500&auto=format&fit=crop&q=60',
},
];
export const SupportChatScreen: React.FC = () => {
const { colors } = useAppTheme();
const styles = getStyles(colors);
const route = useRoute<SupportChatRouteProp>();
const navigation = useNavigation<SupportChatNavProp>();
const { ticketId } = route.params;
const {
currentTicket,
messages,
loading,
isAgentTyping,
fetchTicketDetails,
sendMessage,
reopenTicket,
emitTypingStart,
emitTypingStop,
} = useCustomerSupport(ticketId);
const [inputText, setInputText] = useState('');
const [selectedAttachmentUrl, setSelectedAttachmentUrl] = useState<string | null>(null);
const [attachmentModalVisible, setAttachmentModalVisible] = useState(false);
const flatListRef = useRef<FlatList>(null);
const typingTimeoutRef = useRef<any>(null);
useEffect(() => {
if (ticketId) {
fetchTicketDetails(ticketId);
}
}, [ticketId, fetchTicketDetails]);
// Auto scroll to bottom when messages change
useEffect(() => {
if (messages.length > 0) {
setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 100);
}
}, [messages]);
const handleSend = async () => {
if (!inputText.trim() && !selectedAttachmentUrl) return;
try {
const textToSend = inputText.trim();
const attachmentsToSend = selectedAttachmentUrl ? [selectedAttachmentUrl] : undefined;
setInputText('');
setSelectedAttachmentUrl(null);
emitTypingStop();
await sendMessage(ticketId, textToSend, attachmentsToSend);
} catch (error) {
Alert.alert('Error', 'Failed to send message. Please try again.');
}
};
const handleInputChange = (text: string) => {
setInputText(text);
// Emit typing start and manage typing stop timeout
emitTypingStart();
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
emitTypingStop();
}, 2000);
};
const handleReopen = async () => {
try {
await reopenTicket(ticketId);
Alert.alert('Success', 'Ticket has been reopened.');
} catch (error) {
Alert.alert('Error', 'Failed to reopen ticket.');
}
};
const getStatusColor = (status?: string) => {
switch (status) {
case 'OPEN':
return { bg: '#E3F2FD', text: '#1E88E5' }; // Blue
case 'IN_PROGRESS':
return { bg: '#FFF3E0', text: '#FB8C00' }; // Amber
case 'RESOLVED':
return { bg: '#E8F5E9', text: '#43A047' }; // Green
case 'CLOSED':
return { bg: '#ECEFF1', text: '#546E7A' }; // Grey
default:
return { bg: '#F5F5F5', text: '#9E9E9E' };
}
};
const statusStyle = getStatusColor(currentTicket?.status);
if (loading && !currentTicket) {
return (
<View style={styles.container}>
<Header title="Support Ticket" onBack={() => navigation.goBack()} />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading support ticket...</Text>
</View>
</View>
);
}
const isClosed = currentTicket?.status === 'RESOLVED' || currentTicket?.status === 'CLOSED';
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<Header
title={`Support Chat`}
onBack={() => navigation.goBack()}
rightComponent={
currentTicket && (
<View style={[styles.statusBadge, { backgroundColor: statusStyle.bg }]}>
<Text style={[styles.statusText, { color: statusStyle.text }]}>
{currentTicket.status.replace('_', ' ')}
</Text>
</View>
)
}
/>
{currentTicket && (
<View style={styles.ticketInfoBar}>
<Text style={styles.ticketSubject}>{currentTicket.subject}</Text>
<Text style={styles.ticketMeta}>
ID: #{currentTicket.ticketNumber} | Category: {currentTicket.category.replace('_', ' ')}
</Text>
</View>
)}
<FlatList
ref={flatListRef}
data={messages}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.chatList}
renderItem={({ item }) => {
const isCustomer = item.sender.roleEnum === 'CUSTOMER';
return (
<View
style={[
styles.messageRow,
isCustomer ? styles.customerRow : styles.agentRow,
]}
>
<View
style={[
styles.bubble,
isCustomer ? styles.customerBubble : styles.agentBubble,
]}
>
{!isCustomer && (
<Text style={styles.senderName}>{item.sender.name} (Support)</Text>
)}
{item.attachments && item.attachments.map((att: string, idx: number) => (
<Image
key={idx}
source={{ uri: getFullUrl(att) }}
style={styles.attachmentImage}
resizeMode="cover"
/>
))}
{item.message.trim() ? (
<Text
style={[
styles.messageText,
isCustomer ? styles.customerText : styles.agentText,
]}
>
{item.message}
</Text>
) : null}
<Text
style={[
styles.timeText,
isCustomer ? styles.customerTime : styles.agentTime,
]}
>
{formatTime(item.createdAt)}
</Text>
</View>
</View>
);
}}
onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })}
/>
{isAgentTyping && (
<View style={styles.typingContainer}>
<ActivityIndicator size="small" color={colors.textSecondary} />
<Text style={styles.typingText}>Agent is typing...</Text>
</View>
)}
{/* Selected attachment preview */}
{selectedAttachmentUrl && (
<View style={styles.previewContainer}>
<View style={styles.previewImageWrapper}>
<Image source={{ uri: selectedAttachmentUrl }} style={styles.previewImage} />
<TouchableOpacity
style={styles.removePreviewBtn}
onPress={() => setSelectedAttachmentUrl(null)}
>
<Text style={styles.removePreviewText}></Text>
</TouchableOpacity>
</View>
</View>
)}
{/* Input or Reopen prompt */}
{isClosed ? (
<View style={styles.reopenContainer}>
<Text style={styles.reopenTitle}>
This ticket has been resolved or closed.
</Text>
<TouchableOpacity style={styles.reopenBtn} onPress={handleReopen} activeOpacity={0.8}>
<Text style={styles.reopenBtnText}>Reopen Ticket</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.inputContainer}>
<TouchableOpacity
style={styles.attachmentBtn}
onPress={() => setAttachmentModalVisible(true)}
>
<Text style={{ fontSize: 22 }}>📎</Text>
</TouchableOpacity>
<TextInput
style={styles.input}
value={inputText}
onChangeText={handleInputChange}
placeholder="Type a message..."
placeholderTextColor={colors.textSecondary}
multiline
onBlur={emitTypingStop}
/>
<TouchableOpacity
style={[
styles.sendBtn,
(!inputText.trim() && !selectedAttachmentUrl) && styles.sendBtnDisabled,
]}
onPress={handleSend}
disabled={!inputText.trim() && !selectedAttachmentUrl}
activeOpacity={0.8}
>
<Text style={styles.sendIcon}></Text>
</TouchableOpacity>
</View>
)}
{/* Attachment Modal */}
<Modal
visible={attachmentModalVisible}
transparent
animationType="fade"
onRequestClose={() => setAttachmentModalVisible(false)}
>
<View
style={{
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
}}
>
<View
style={{
backgroundColor: colors.cardBg,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
}}
>
<Text
style={{
fontSize: 18,
fontWeight: 'bold',
color: colors.text,
marginBottom: 16,
textAlign: 'center',
}}
>
Select Mock Attachment
</Text>
{MOCK_ATTACHMENTS.map((item) => (
<TouchableOpacity
key={item.id}
style={{
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
}}
onPress={() => {
setSelectedAttachmentUrl(item.url);
setAttachmentModalVisible(false);
}}
>
<Text style={{ fontSize: 24, marginRight: 12 }}>🖼</Text>
<Text style={{ fontSize: 16, color: colors.text }}>{item.label}</Text>
</TouchableOpacity>
))}
<TouchableOpacity
style={{
marginTop: 16,
paddingVertical: 12,
backgroundColor: colors.border,
borderRadius: 10,
alignItems: 'center',
}}
onPress={() => setAttachmentModalVisible(false)}
>
<Text style={{ fontSize: 16, fontWeight: 'bold', color: colors.text }}>Cancel</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</KeyboardAvoidingView>
);
};
export default SupportChatScreen;