💬 Parent Chat
This document covers the comprehensive chat system for parents in the Com DEALL mobile application, including real-time messaging, media sharing, therapist communication, and chat management.
🎯 Overview
The parent chat system enables real-time communication between parents and therapists, supporting text messages, media sharing, and group conversations. The system includes message status tracking, online presence, and notification management.
🏗️ Architecture
Chat System Components
Parent Chat System
├── Chat List & Contacts
├── Real-time Messaging
├── Media Sharing
├── Message Status
├── Online Presence
└── Notifications
Data Flow
Message Input → Socket.IO → Server → Database → Real-time Sync → UI Update
↓ ↓ ↓ ↓ ↓ ↓
Parent Types → WebSocket → Processing → Storage → Live Update → Message Display
📱 Mobile Implementation
Parent Chat Screen
// ParentChat.tsx - Main chat interface
const ParentChat = ({ navigation }: Props) => {
const { activeChildId, setActiveChildId } = useChildContext();
const [messageType, setMessageType] = useState<MessageType>('ALL');
const [viewContacts, setViewContacts] = useState(false);
const { parentId, userId } = useSelector(selectUserData);
const { data, loading, error, refetch } = useGetChildrenForAssessmentQuery({
fetchPolicy: 'cache-and-network',
variables: {
parent_id: parentId,
},
});
const filteredChildren = useMemo(
() =>
data?.child.map(c => {
return {
name: c.name,
age: c.age,
profile_image: {
path: c.profile_image?.path,
},
id: c.id,
};
}) ?? [],
[data?.child],
);
const selectedChild = useMemo(
() => filteredChildren.find(c => c.id === activeChildId),
[filteredChildren, activeChildId],
);
const {
data: therapists,
refetch: refetchTherapist,
loading: loadingTherapist,
} = useGetChildsTherapistsQuery({
variables: { child_id: selectedChild?.id },
skip: !selectedChild?.id,
});
const { data: chatEligibility } = useCheckChatEligibilitySubscription({
variables: { child_id: selectedChild?.id, chat_feature: 'chat' },
fetchPolicy: 'network-only',
skip: !selectedChild?.id,
});
const { data: allChats, loading: loadingAllChats } = useChatAvailableSubscription({
variables: { child_id: selectedChild?.id },
fetchPolicy: 'network-only',
skip: !selectedChild?.id,
});
const { data: totalUnread } = useUnreadCountTotalSubscription({
variables: { child_id: selectedChild?.id },
fetchPolicy: 'network-only',
skip: !selectedChild?.id,
});
// Chat data processing with comprehensive filtering and eligibility checks
const chatsAvailable = (allChats?.chat_messages.length ?? 0) >= 1;
const isChildPremium = (chatEligibility?.child_subscription.length ?? 0) > 0;
const toggleView = () => setViewContacts(!viewContacts);
// Chat eligibility and availability logic
const chatEligibilityData = useMemo(() => {
return {
isEligible: isChildPremium,
hasActiveChats: chatsAvailable,
totalUnread: totalUnread?.chat_messages?.length ?? 0
};
}, [isChildPremium, chatsAvailable, totalUnread]);
};
Chat List Component
// ChatList.tsx - Chat conversations list
const ChatList = ({ data, toggleView, messageType, clearFilter, onRefresh, refreshing }) => {
const { userId } = useSelector(selectUserData);
const { data: totalCount } = useUnreadCountTotalSubscription({
variables: { my_id: userId },
});
// Chat list data processing with comprehensive contact management
const combinedContacts = data?.child_therapists?.map(therapist => {
const therapistData = {
name: therapist?.therapist?.user?.name ?? '',
id: therapist?.therapist?.id ?? '',
user: { id: therapist?.therapist?.user?.id ?? '' },
profile_picture: therapist?.therapist?.user?.profile_picture?.path ?? '',
type: 'therapist',
};
return therapistData;
});
// Chat navigation logic with comprehensive user data
const handleChatNavigation = (contact: any) => {
navigation.navigate('parent/chat-screen', {
user: {
childId: selectedChild?.id,
name: contact.name,
img: contact.profile_picture,
userId: contact.user.id,
userType: 'PARENT',
recipientType: 'THERAPIST',
}
});
};
};
Chat Screen
// ChatScreen.tsx - Individual chat interface
const ChatScreen = ({ navigation, route }: ScreenProps) => {
const { userId } = useSelector(selectUserData);
const [messages, setMessages] = useState([]);
const [isUploading, setIsUploading] = useState(false);
const [isOnline, setIsOnline] = useState(false);
const { data: eligibilityData } = useCheckChatEligibilitySubscription({
variables: {
child_id: route?.params?.user?.childId,
chat_feature: 'chat',
},
fetchPolicy: 'network-only',
});
const isChildPremium = (eligibilityData?.child_subscription.length ?? 0) > 0;
const [markAsRead] = useMarkAsReadMutation();
useEffect(() => {
markAsRead({
variables: {
sender_id: route?.params?.user?.userId,
my_user_id: userId,
child_id: route?.params?.user?.childId ?? '',
},
});
}, [markAsRead, route?.params?.user?.childId, route?.params?.user?.userId, userId]);
const { loading } = useGetChatBetweenUsersQuery({
variables: {
sender_id: route?.params?.user?.userId,
my_user_id: userId,
child_id: route?.params?.user?.childId ?? '',
},
onCompleted: (data) => {
const formattedMessages = data?.chat_messages?.map(msg => ({
_id: msg.id,
text: msg.message,
createdAt: new Date(msg.created_at),
user: {
_id: msg.sender_id,
name: msg.sender_id === userId ? 'You' : route?.params?.user?.name,
avatar: msg.sender_id === userId ? null : route?.params?.user?.img,
},
image: msg.media?.path,
video: msg.media?.type === 'video' ? msg.media?.path : null,
audio: msg.media?.type === 'audio' ? msg.media?.path : null,
document: msg.media?.type === 'document' ? msg.media?.path : null,
}));
setMessages(formattedMessages || []);
},
});
// Message sending logic with comprehensive validation and premium checks
const onSend = useCallback((messages = []) => {
if (!isChildPremium) {
showUnsubscribedToast();
return;
}
const message = messages[0];
const messageData = {
message: message.text,
sender_id: userId,
recipient_id: route?.params?.user?.userId,
child_id: route?.params?.user?.childId,
media_id: message.media_id,
};
socket.emit('send_message', messageData);
setMessages(previousMessages => GiftedChat.append(previousMessages, messages));
}, [userId, route?.params?.user?.userId, route?.params?.user?.childId, isChildPremium]);
// Message formatting logic with comprehensive media support
const formatMessages = (chatMessages: any[]) => {
return chatMessages?.map(msg => ({
_id: msg.id,
text: msg.message,
createdAt: new Date(msg.created_at),
user: {
_id: msg.sender_id,
name: msg.sender_id === userId ? 'You' : route?.params?.user?.name,
avatar: msg.sender_id === userId ? null : route?.params?.user?.img,
},
image: msg.media?.path,
video: msg.media?.type === 'video' ? msg.media?.path : null,
audio: msg.media?.type === 'audio' ? msg.media?.path : null,
document: msg.media?.type === 'document' ? msg.media?.path : null,
}));
};
};
📱 Media Sharing
Media Upload
// Media sharing functionality
const useMediaSharing = () => {
const [isUploading, setIsUploading] = useState(false);
const pickImage = async () => {
try {
const result = await ImagePicker.launchImageLibrary({
mediaType: 'photo',
quality: 0.8,
maxWidth: 1024,
maxHeight: 1024,
});
if (result.assets?.[0]) {
await uploadMedia(result.assets[0]);
}
} catch (error) {
console.error('Error picking image:', error);
}
};
const pickVideo = async () => {
try {
const result = await ImagePicker.launchImageLibrary({
mediaType: 'video',
quality: 0.8,
});
if (result.assets?.[0]) {
await uploadMedia(result.assets[0]);
}
} catch (error) {
console.error('Error picking video:', error);
}
};
const pickDocument = async () => {
try {
const result = await DocumentPicker.pick({
type: [DocumentPicker.types.pdf, DocumentPicker.types.doc, DocumentPicker.types.docx],
});
if (result[0]) {
await uploadMedia(result[0]);
}
} catch (error) {
console.error('Error picking document:', error);
}
};
const uploadMedia = async (media: any) => {
setIsUploading(true);
try {
const formData = new FormData();
formData.append('files', {
name: media.fileName || 'media',
uri: media.uri,
type: media.type,
});
const response = await axios.post(
process.env.EXPO_PUBLIC_MEDIA_UPLOAD_PUBLIC!,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${mmkvStorage.get('accessToken')}`,
},
}
);
return response.data.data[0];
} catch (error) {
console.error('Error uploading media:', error);
throw error;
} finally {
setIsUploading(false);
}
};
return {
pickImage,
pickVideo,
pickDocument,
uploadMedia,
isUploading,
};
};
Media Preview
// MediaPreview.tsx - Media preview component
const MediaPreview = ({ media, onClose }) => {
// Media preview logic with comprehensive type handling
const renderMedia = () => {
switch (media.type) {
case 'image':
return {
type: 'image',
source: media.path,
resizeMode: 'contain'
};
case 'video':
return {
type: 'video',
source: media.path,
useNativeControls: true,
resizeMode: 'contain'
};
case 'document':
return {
type: 'document',
path: media.path,
name: media.name,
icon: 'PDF_ICON'
};
default:
return null;
}
};
const handleClose = () => {
onClose();
};
};
🔄 Real-time Communication
Socket.IO Integration
// socket.ts - Socket.IO configuration
import { io } from 'socket.io-client';
const socket = io(process.env.EXPO_PUBLIC_SOCKET_URL!, {
auth: {
token: mmkvStorage.get('accessToken'),
},
transports: ['websocket'],
});
socket.on('connect', () => {
console.log('Connected to chat server');
});
socket.on('disconnect', () => {
console.log('Disconnected from chat server');
});
socket.on('message_received', (message) => {
// Handle incoming message
handleIncomingMessage(message);
});
socket.on('typing_start', (data) => {
// Handle typing indicator
handleTypingStart(data);
});
socket.on('typing_stop', (data) => {
// Handle typing stop
handleTypingStop(data);
});
export { socket };
Message Status Tracking
// Message status management
const useMessageStatus = () => {
const [messageStatus, setMessageStatus] = useState({});
const updateMessageStatus = (messageId: string, status: 'sent' | 'delivered' | 'read') => {
setMessageStatus(prev => ({
...prev,
[messageId]: status
}));
};
const markAsRead = (messageId: string) => {
updateMessageStatus(messageId, 'read');
};
const markAsDelivered = (messageId: string) => {
updateMessageStatus(messageId, 'delivered');
};
return {
messageStatus,
updateMessageStatus,
markAsRead,
markAsDelivered,
};
};
📱 Mobile-Specific Features
Touch Interactions
// Chat touch interactions
const useChatGestures = () => {
const handleMessageLongPress = (message: Message) => {
// Long press to show message options
showMessageOptions(message);
};
const handleMessageSwipe = (message: Message, direction: 'left' | 'right') => {
// Swipe to reply or forward
if (direction === 'left') {
showReplyOptions(message);
} else {
showForwardOptions(message);
}
};
const handleMediaPress = (media: Media) => {
// Press to preview media
showMediaPreview(media);
};
return {
handleMessageLongPress,
handleMessageSwipe,
handleMediaPress,
};
};
Offline Chat
// Offline chat capabilities
const useOfflineChat = () => {
const [offlineMessages, setOfflineMessages] = useState([]);
const saveOfflineMessage = (message: Message) => {
const offlineMessage = {
...message,
id: `offline_${Date.now()}`,
isOffline: true,
createdAt: new Date().toISOString()
};
setOfflineMessages(prev => [...prev, offlineMessage]);
mmkvStorage.set('offline_messages', JSON.stringify([...offlineMessages, offlineMessage]));
};
const syncOfflineMessages = async () => {
if (offlineMessages.length === 0) return;
try {
for (const message of offlineMessages) {
await sendMessage(message);
}
setOfflineMessages([]);
mmkvStorage.delete('offline_messages');
toast.show({ text: 'Offline messages synced successfully' });
} catch (error) {
toast.show({ text: 'Failed to sync offline messages' });
}
};
return {
saveOfflineMessage,
syncOfflineMessages,
offlineMessages,
};
};
🎨 UI Components
Chat Preview Card
// ChatPreviewCard.tsx
const ChatPreviewCard = ({
contact,
onPress
}) => {
const { colors } = useTheme<Theme>();
// Chat preview card logic with comprehensive data display
const handleCardPress = () => {
onPress(contact);
};
// Contact data processing for display
const contactData = useMemo(() => {
return {
name: contact.name,
profilePicture: contact.profile_picture,
lastMessage: contact.lastMessage,
lastMessageTime: format(new Date(contact.lastMessageTime), 'HH:mm'),
unreadCount: contact.unreadCount,
hasUnread: contact.unreadCount > 0
};
}, [contact]);
};
🔧 GraphQL Integration
Chat Queries
# Get chat users
query GetChildrenForAssessment($parent_id: String!) {
child(
where: { parent_id: { _eq: $parent_id } }
order_by: { created_at: desc }
) {
id
name
profile_image { path }
child_therapists {
therapist {
id
user { id name profile_picture { path } }
}
}
}
}
# Get chat messages
query GetChatBetweenUsers($sender_id: String!, $my_user_id: String!, $child_id: String!) {
chat_messages(
where: {
_or: [
{ sender_id: { _eq: $sender_id }, recipient_id: { _eq: $my_user_id } }
{ sender_id: { _eq: $my_user_id }, recipient_id: { _eq: $sender_id } }
]
child_id: { _eq: $child_id }
}
order_by: { created_at: asc }
) {
id
message
sender_id
recipient_id
child_id
created_at
media {
id
path
type
}
}
}
Chat Mutations
# Send message
mutation SendMessage($input: chat_messages_insert_input!) {
insert_chat_messages_one(object: $input) {
id
message
sender_id
recipient_id
child_id
created_at
}
}
# Mark as read
mutation MarkAsRead($sender_id: String!, $my_user_id: String!, $child_id: String!) {
markAsRead(sender_id: $sender_id, my_user_id: $my_user_id, child_id: $child_id) {
success
}
}
🎯 Best Practices
Chat Management
- Real-time Updates: Provide live message synchronization
- Message Status: Track message delivery and read status
- Media Sharing: Support various media types
- Offline Support: Queue messages when offline
User Experience
- Visual Feedback: Clear message status indicators
- Touch Interactions: Intuitive chat gestures
- Media Preview: Easy media viewing and sharing
- Notifications: Timely message notifications
Performance
- Message Caching: Cache chat history locally
- Lazy Loading: Load messages on demand
- Optimistic Updates: Update UI before server confirmation
- Background Sync: Sync messages in background
🎯 Summary
The parent chat system provides comprehensive functionality for real-time communication with therapists, including media sharing, message status tracking, and offline capabilities. The mobile implementation includes touch-optimized interfaces, real-time synchronization, and seamless media handling for a complete chat experience.