Skip to main content

πŸ’¬ 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

  1. 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

  1. 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

  1. 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 FieldContact List FieldPurpose
child.namenameDisplay name in chat list
child.ididChild identifier for filtering messages
child.parent.user.iduser.idParent's user ID (actual message recipient)
child.profile_image.pathprofile_pictureAvatar image URL
HardcodedtypeContact category identifier

Why this structure?

  • name: Display in chat list
  • id: Child identifier (for message filtering)
  • user.id: Parent's user ID (actual message recipient)
  • profile_picture: Show avatar in chat list
  • type: 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​

FunctionalityDescription
Eligibility CheckSubscribes to real-time updates on child’s chat eligibility. Non-premium users are blocked from sending messages.
Mark as ReadAutomatically marks all messages as read when the chat opens.
Chat FetchingLoads past messages and formats them for the GiftedChat component (with full media support: image, video, audio, documents).
Send Message LogicSends 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​

  1. State Management

    • isUploading tracks whether a media upload is in progress.
  2. Media Selection

    • pickImage() β†’ Opens device gallery for images.
    • pickVideo() β†’ Opens device gallery for videos.
    • pickDocument() β†’ Opens file picker for documents (PDF, DOC, DOCX).
  3. Media Upload

    • uploadMedia(media) β†’ Uploads the selected file via axios POST request.
    • Uses FormData for multipart upload.
    • Attaches Authorization header with stored access token.
    • Returns uploaded media info on success.
  4. Error Handling

    • Each picker has a try/catch block to handle errors gracefully.
    • Upload errors are logged and re-thrown for higher-level handling.
  5. Returned API

    • Exposes pickers (pickImage, pickVideo, pickDocument), uploadMedia, and isUploading for UI components.

πŸ“ 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​

  1. Purpose

    • Provides a unified interface to preview different media types: images, videos, and documents.
  2. Media Type Handling

    • Image: Rendered with resizeMode: 'contain'.
    • Video: Rendered with native controls (useNativeControls: true) and resizeMode: 'contain'.
    • Document: Shows document info (path, name, and icon, e.g., PDF_ICON).
  3. Close Handling

    • handleClose() calls the passed onClose callback to close the preview.
  4. Extensibility

    • Additional media types can be easily added in the renderMedia switch statement.

πŸ“ 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

🎯 Next Steps​