π¬ Therapist Chat Feature
This document covers the comprehensive chat system for therapists in the Com DEALL mobile application, including real-time messaging, media sharing, and communication management.
π― Overviewβ
The therapist chat system enables real-time communication between therapists and parents, supporting text messages, media sharing, and group conversations. The system includes message status tracking, online presence, and notification management.
ποΈ Architectureβ
Chat System Componentsβ
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
β β β β β β
User Types β WebSocket β Processing β Storage β Live Update β Message Display
π± Mobile Implementationβ
Therapist Chat Screen Logicβ
Chat Interface Management:
- System manages real-time chat conversations between therapists and parents
- Implements dual view mode: Chat List for active conversations and Contacts List for available contacts
- Handles message type filtering (All, Unread, Important) for better conversation management
- Provides real-time subscription updates for new messages and unread counts
- Supports pull-to-refresh for manual data updates
View State Management:
- Chat List View: Displays active conversations with unread message indicators
- Contacts View: Shows available parent contacts for starting new conversations
- Toggle Logic: Seamlessly switches between chat list and contacts view
- Message Filtering: Filters conversations by message type and status
- Real-time Updates: Automatically updates when new messages arrive
Data Fetching Strategy:
- Chat Users Query: Fetches all parents associated with therapist's children
- Chat Messages Subscription: Real-time updates for new messages and unread counts
- Contact Management: Manages parent contact information and profile data
- Message Status: Tracks read/unread status and message delivery
- Online Presence: Monitors parent online status for real-time indicators
// TherapistChat.tsx - Comprehensive chat interface with real-time messaging
const TherapistChat = ({ navigation }: ScreenProps) => {
const { therapistId, userId } = useSelector(selectUserData);
const [messageType, setMessageType] = useState<MessageType>('ALL');
const [viewContacts, setViewContacts] = useState(false);
const { data, loading, error, refetch } = useTherapistChatUsersQuery({
variables: { therapist_id: therapistId },
skip: !therapistId,
});
const { data: totalChats } = useCheckTherapistChatsSubscription({
variables: { user_id: userId },
fetchPolicy: 'network-only',
});
const chatsAvailable = (totalChats?.chat_messages.length ?? 0) >= 1;
const toggleView = () => setViewContacts(!viewContacts);
};
Chat List Componentβ
const ChatList = ({ data, toggleView, messageType, clearFilter, onRefresh, refreshing }) => {
// 1. Get current user ID from Redux store
const { userId } = useSelector(selectUserData);
// 2. Real-time subscription for unread message count
const { data: totalCount } = useUnreadCountTherapistSubscription({
variables: { my_id: userId },
});
// 3. Transform child_therapist data into contact list format
const combinedContacts = data?.child_therapist?.map(child => ({
name: child?.child?.name ?? '',
id: child?.child?.id ?? '',
user: { id: child?.child?.parent?.user?.id ?? '' },
profile_picture: child?.child?.profile_image?.path ?? '',
type: 'child',
}));
};
π Explanation of Each Part
- Get Current User ID
const { userId } = useSelector(selectUserData);
Purpose: Extracts the logged-in therapist's user ID from Redux state Why needed: To identify which user's unread messages to fetch
- Real-time Unread Count Subscription
const { data: totalCount } = useUnreadCountTherapistSubscription({
variables: { my_id: userId },
});
Purpose: Subscribes to real-time updates of unread message counts
How it works:
- Uses GraphQL subscription (WebSocket)
- Automatically updates when new messages arrive
- Shows badge/indicator on chat list items
Example: If a parent sends a message, the unread count updates instantly without refreshing
- Transform Data into Contact Format
const combinedContacts = data?.child_therapist?.map(child => ({
name: child?.child?.name ?? '', // Child's name
id: child?.child?.id ?? '', // Child ID
user: { id: child?.child?.parent?.user?.id ?? '' }, // Parent's user ID (for messaging)
profile_picture: child?.child?.profile_image?.path ?? '', // Child's profile image
type: 'child', // Contact type
}));
Purpose: Converts database structure into UI-friendly format
Transformation Mappingβ
| Database Field | Contact List Field | Purpose |
|---|---|---|
child.name | name | Display name in chat list |
child.id | id | Child identifier for filtering messages |
child.parent.user.id | user.id | Parent's user ID (actual message recipient) |
child.profile_image.path | profile_picture | Avatar image URL |
| Hardcoded | type | Contact category identifier |
Why this structure?
name: Display in chat listid: Child identifier (for message filtering)user.id: Parent's user ID (actual message recipient)profile_picture: Show avatar in chat listtype: Identifies contact category
Nullish Coalescing (??): Provides empty string fallback if data is null or undefined, preventing app crashes
Core Purpose
Displays an individual chat interface between two users (typically a parent and therapist), handling:
Fetching chat history
Sending new messages
Marking messages as read
Checking subscription (premium) eligibility
βοΈ Key Logicβ
const ChatScreen = ({ route }: ScreenProps) => {
const { userId } = useSelector(selectUserData);
const [messages, setMessages] = useState([]);
// β
Check childβs premium eligibility for chat
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;
// β
Mark messages as read when opening chat
const [markAsRead] = useMarkAsReadMutation();
useEffect(() => {
markAsRead({
variables: {
sender_id: route?.params?.user?.userId,
my_user_id: userId,
child_id: route?.params?.user?.childId ?? '',
},
});
}, []);
// β
Fetch chat history between users
useGetChatBetweenUsersQuery({
variables: {
sender_id: route?.params?.user?.userId,
my_user_id: userId,
child_id: route?.params?.user?.childId ?? '',
},
onCompleted: (data) => {
const formatted = 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(formatted || []);
},
});
// β
Send new message (blocked if not premium)
const onSend = useCallback((messages = []) => {
if (!isChildPremium) return showUnsubscribedToast();
const msg = messages[0];
socket.emit('send_message', {
message: msg.text,
sender_id: userId,
recipient_id: route?.params?.user?.userId,
child_id: route?.params?.user?.childId,
media_id: msg.media_id,
});
setMessages(prev => GiftedChat.append(prev, messages));
}, [userId, isChildPremium]);
};
βοΈ Core Purposeβ
Displays an individual chat interface between two users, handling:
- Fetching chat history
- Sending new messages
- Marking messages as read
- Checking subscription (premium) eligibility
π§ Key Logic Explanationβ
| Functionality | Description |
|---|---|
| Eligibility Check | Subscribes to real-time updates on childβs chat eligibility. Non-premium users are blocked from sending messages. |
| Mark as Read | Automatically marks all messages as read when the chat opens. |
| Chat Fetching | Loads past messages and formats them for the GiftedChat component (with full media support: image, video, audio, documents). |
| Send Message Logic | Sends text/media messages through socket only if the child has a premium plan, and instantly appends them to the UI. |
π± Media Sharingβ
Media Uploadβ
π Key Logicβ
-
State Management
isUploadingtracks whether a media upload is in progress.
-
Media Selection
pickImage()β Opens device gallery for images.pickVideo()β Opens device gallery for videos.pickDocument()β Opens file picker for documents (PDF, DOC, DOCX).
-
Media Upload
uploadMedia(media)β Uploads the selected file viaaxiosPOST request.- Uses
FormDatafor multipart upload. - Attaches
Authorizationheader with stored access token. - Returns uploaded media info on success.
-
Error Handling
- Each picker has a
try/catchblock to handle errors gracefully. - Upload errors are logged and re-thrown for higher-level handling.
- Each picker has a
-
Returned API
- Exposes pickers (
pickImage,pickVideo,pickDocument),uploadMedia, andisUploadingfor UI components.
- Exposes pickers (
π Code Snippetβ
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β
π Key Logicβ
-
Purpose
- Provides a unified interface to preview different media types: images, videos, and documents.
-
Media Type Handling
- Image: Rendered with
resizeMode: 'contain'. - Video: Rendered with native controls (
useNativeControls: true) andresizeMode: 'contain'. - Document: Shows document info (path, name, and icon, e.g., PDF_ICON).
- Image: Rendered with
-
Close Handling
handleClose()calls the passedonClosecallback to close the preview.
-
Extensibility
- Additional media types can be easily added in the
renderMediaswitch statement.
- Additional media types can be easily added in the
π Code Snippetβ
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 & Mobile Chat Features
π Key Logicβ
1. Socket.IO Integrationβ
- Connects to chat server using
Socket.IO. - Authenticates with access token.
- Listens to events:
connect/disconnectβ Log connection status.message_receivedβ Handle incoming messages.typing_start/typing_stopβ Show typing indicators.
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', handleIncomingMessage);
socket.on('typing_start', handleTypingStart);
socket.on('typing_stop', handleTypingStop);
export { socket };
2. Message Status Trackingβ
- Tracks message delivery/read status.
- Functions:
updateMessageStatus(messageId, status)β Update status.markAsRead(messageId)β Mark message as read.markAsDelivered(messageId)β Mark message as delivered.
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 };
};
3. Mobile-Specific Featuresβ
Touch Interactionsβ
- Handles gestures like long press, swipe, and media press.
const useChatGestures = () => {
const handleMessageLongPress = (message: Message) => showMessageOptions(message);
const handleMessageSwipe = (message: Message, direction: 'left' | 'right') => {
if (direction === 'left') showReplyOptions(message);
else showForwardOptions(message);
};
const handleMediaPress = (media: Media) => showMediaPreview(media);
return { handleMessageLongPress, handleMessageSwipe, handleMediaPress };
};
Offline Chatβ
- Saves messages offline when no network is available.
- Syncs offline messages when back online.
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) 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 };
};
π§ GraphQL Integrationβ
Chat Queriesβ
# Get chat users
query TherapistChatUsers($therapist_id: String!) {
child_therapist(
where: { therapist_id: { _eq: $therapist_id } }
order_by: { created_at: desc }
) {
child {
id
name
profile_image { path }
parent {
user { id name }
}
}
}
}
# 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