Overview
The Twigz chat system provides real-time messaging between customers, stores, and admin support. This guide covers the complete implementation from basic messaging to advanced features like conversation transfers and typing indicators.
Architecture
The chat system consists of three main components:
Customer Support Direct messaging between customers and stores
Admin Support Admin intervention and conversation management
Transfer System Seamless conversation transfers between support levels
Core Concepts
Conversation Types
Purpose : Direct communication between customers and store ownersRequirements :
Customer must have at least one non-cancelled order with the store
Direct messaging must be enabled globally and for the specific store
Store must have direct messaging enabled in their settings
Use Cases :
Order-related questions
Product inquiries
Delivery issues
General store communication
Purpose : Customer support through platform administratorsRequirements :
Admin support must be enabled in platform settings
Available as fallback when direct store messaging is not possible
Use Cases :
Platform-related issues
Escalated store problems
General support requests
Technical difficulties
Message Types
Standard text communication with full UTF-8 support
Images with automatic validation and compression (max 5MB)
Automated notifications and status updates
Messages indicating conversation transfers between support levels
Implementation Guide
1. Basic Chat Setup
React Hook
Vue Composition API
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
export function useChat () {
// Get user's conversations
const conversations = useQuery ( api . shared . chat . getConversations , {
status: "active" ,
limit: 50
});
// Get unread count
const unreadCount = useQuery ( api . shared . chat . getUnreadCount , {});
// Mutations
const createConversation = useMutation ( api . shared . chat . createOrGetConversation );
const sendMessage = useMutation ( api . shared . chat . sendMessage );
const markAsRead = useMutation ( api . shared . chat . markMessagesAsRead );
return {
conversations ,
unreadCount ,
createConversation ,
sendMessage ,
markAsRead
};
}
2. Real-time Message Display
React Component
Vue Component
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
interface ChatMessagesProps {
conversationId : string ;
}
export function ChatMessages ({ conversationId } : ChatMessagesProps ) {
const messages = useQuery ( api . shared . chat . getMessages , {
conversationId ,
limit: 50
});
const typingIndicators = useQuery ( api . shared . chat . getTypingIndicators , {
conversationId
});
const updateTyping = useMutation ( api . shared . chat . updateTypingStatus );
const markAsRead = useMutation ( api . shared . chat . markMessagesAsRead );
// Mark messages as read when component mounts
useEffect (() => {
if ( messages ?. messages . length ) {
markAsRead ({ conversationId });
}
}, [ conversationId , messages ?. messages . length ]);
return (
< div className = "chat-container" >
< div className = "messages" >
{ messages ?. messages . map (( message ) => (
< MessageBubble key = {message. _id } message = { message } />
))}
</ div >
{ typingIndicators ?. length > 0 && (
< div className = "typing-indicators" >
{ typingIndicators . map (( indicator ) => (
< div key = {indicator. userId } className = "typing-indicator" >
{ indicator . userName } is typing ...
</ div >
))}
</ div >
)}
</ div >
);
}
React Component
Vue Component
import { useState , useEffect , useCallback } from 'react' ;
import { useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
interface MessageInputProps {
conversationId : string ;
}
export function MessageInput ({ conversationId } : MessageInputProps ) {
const [ message , setMessage ] = useState ( '' );
const [ isTyping , setIsTyping ] = useState ( false );
const sendMessage = useMutation ( api . shared . chat . sendMessage );
const updateTyping = useMutation ( api . shared . chat . updateTypingStatus );
// Debounced typing indicator
const debouncedTypingUpdate = useCallback (
debounce (( typing : boolean ) => {
updateTyping ({ conversationId , isTyping: typing });
setIsTyping ( typing );
}, 500 ),
[ conversationId ]
);
const handleInputChange = ( value : string ) => {
setMessage ( value );
// Update typing status
if ( value . length > 0 && ! isTyping ) {
debouncedTypingUpdate ( true );
} else if ( value . length === 0 && isTyping ) {
debouncedTypingUpdate ( false );
}
};
const handleSend = async () => {
if ( ! message . trim ()) return ;
try {
await sendMessage ({
conversationId ,
content: message . trim (),
messageType: "text"
});
setMessage ( '' );
debouncedTypingUpdate ( false );
} catch ( error ) {
console . error ( 'Failed to send message:' , error );
}
};
// Cleanup typing indicator when component unmounts
useEffect (() => {
return () => {
if ( isTyping ) {
updateTyping ({ conversationId , isTyping: false });
}
};
}, [ conversationId , isTyping ]);
return (
< div className = "message-input" >
< input
type = "text"
value = { message }
onChange = {(e) => handleInputChange (e.target.value)}
onKeyPress = {(e) => e. key === 'Enter' && handleSend ()}
placeholder = "Type a message..."
/>
< button onClick = { handleSend } disabled = {!message.trim()} >
Send
</ button >
</ div >
);
}
// Debounce utility
function debounce < T extends ( ... args : any []) => void >(
func : T ,
wait : number
) : ( ... args : Parameters < T >) => void {
let timeout : NodeJS . Timeout ;
return ( ... args : Parameters < T >) => {
clearTimeout ( timeout );
timeout = setTimeout (() => func ( ... args ), wait );
};
}
4. Image Message Support
React Hook
Vue Composition API
import { useAction } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
export function useImageUpload () {
const processImage = useAction ( api . shared . imageProcessing . processImage );
const validateImage = useAction ( api . shared . imageProcessing . validateImageUpload );
const sendMessage = useMutation ( api . shared . chat . sendMessage );
const sendImageMessage = async (
conversationId : string ,
imageFile : File
) => {
try {
// 1. Upload to storage
const storageId = await convex . storage . store ( imageFile );
// 2. Validate image
const validation = await validateImage ({
storageId ,
imageType: "chatImage"
});
if ( ! validation . valid ) {
throw new Error ( validation . error );
}
// 3. Process image
const processed = await processImage ({
storageId ,
imageType: "chatImage" ,
quality: 80
});
// 4. Send message
const messageId = await sendMessage ({
conversationId ,
content: "📷 Image" ,
messageType: "image" ,
imageAttachment: {
fileId: processed . newStorageId ,
fileName: imageFile . name ,
fileType: imageFile . type ,
fileSize: processed . compressedSize ,
width: processed . width ,
height: processed . height
}
});
return messageId ;
} catch ( error ) {
console . error ( 'Failed to send image:' , error );
throw error ;
}
};
return { sendImageMessage };
}
5. Admin Chat Management
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
export function AdminChatDashboard () {
const conversations = useQuery ( api . admins . chat . getAdminConversations , {
status: "active" ,
limit: 50
});
const chatStats = useQuery ( api . admins . chat . getChatStats , {});
const transferToStore = useMutation ( api . admins . chat . transferConversationToStore );
const blockConversation = useMutation ( api . admins . chat . blockConversation );
const handleTransferToStore = async ( conversationId : string , storeId : string ) => {
try {
const result = await transferToStore ({
conversationId ,
storeId ,
includeHistory: true
});
console . log ( 'Conversation transferred:' , result . newConversationId );
} catch ( error ) {
console . error ( 'Transfer failed:' , error );
}
};
return (
< div className = "admin-chat-dashboard" >
< div className = "stats" >
< div > Total Conversations : { chatStats ?. totalConversations }</ div >
< div > Active Conversations : { chatStats ?. activeConversations }</ div >
< div > Blocked Conversations : { chatStats ?. blockedConversations }</ div >
</ div >
< div className = "conversations" >
{ conversations ?. map (( conv ) => (
< ConversationCard
key = {conv. _id }
conversation = { conv }
onTransfer = { handleTransferToStore }
onBlock = {() => blockConversation ({ conversationId : conv . _id })}
/>
))}
</ div >
</ div >
);
}
Advanced Features
Conversation Transfers
The system supports seamless conversation transfers between different support levels:
Customer to Store Transfer
When : Customer wants to communicate directly with store
Requirements : Customer must have orders with the store
Process : Creates new store conversation, archives admin conversation
When : Store needs admin intervention
Requirements : Store owner or admin can initiate
Process : Creates new admin conversation, archives store conversation
Option : Include or exclude message history
Default : Admin transfers include history, store transfers exclude
Benefit : Maintains context while allowing fresh start
Chat Settings Management
import { useQuery , useMutation } from 'convex/react' ;
import { api } from './convex/_generated/api' ;
export function ChatSettingsManager () {
const chatSettings = useQuery ( api . shared . chat . getChatSettings , {});
const updateSettings = useMutation ( api . admins . chat . updateChatSettings );
const handleUpdateSettings = async ( newSettings : any ) => {
try {
await updateSettings ( newSettings );
console . log ( 'Settings updated successfully' );
} catch ( error ) {
console . error ( 'Failed to update settings:' , error );
}
};
return (
< div className = "chat-settings" >
< h3 > Chat System Settings </ h3 >
< div className = "setting" >
< label >
< input
type = "checkbox"
checked = {chatSettings?. directMessagingEnabled }
onChange = {(e) => handleUpdateSettings ({
directMessagingEnabled : e . target . checked
})}
/>
Enable Direct Messaging
</ label >
</ div >
< div className = "setting" >
< label >
Max Image Size ( MB ) :
< input
type = "number"
value = { chatSettings ?. maxImageSizeMB }
onChange = {( e ) => handleUpdateSettings ({
maxImageSizeMB : parseInt ( e . target . value )
})}
/>
</ label >
</ div >
< div className = "setting" >
< label >
< input
type = "checkbox"
checked = {chatSettings?. adminSupportEnabled }
onChange = {(e) => handleUpdateSettings ({
adminSupportEnabled : e . target . checked
})}
/>
Enable Admin Support
</ label >
</ div >
</ div >
);
}
Best Practices
Performance
Debounce typing indicators to avoid excessive updates
Use pagination for message loading
Implement proper cleanup for real-time subscriptions
User Experience
Show typing indicators for better engagement
Provide immediate feedback for message status
Handle offline scenarios gracefully
Security
Validate all message content
Implement proper authorization checks
Sanitize user input before display
Scalability
Use efficient database queries with indexes
Implement proper caching strategies
Monitor chat system performance
Error Handling
Problem : Connection lost during message send
Solution : Implement retry logic with exponential backoff
User Feedback : Show message as “pending” until confirmed
Problem : User tries to access unauthorized conversation
Solution : Check permissions before allowing access
User Feedback : Show clear error message with guidance
Problem : Image too large or invalid format
Solution : Validate before upload and provide clear limits
User Feedback : Show specific error message with requirements
The chat system is designed to handle high-volume messaging with real-time updates. Always test thoroughly with multiple concurrent users to ensure performance.
Consider implementing message queuing for offline scenarios and message status indicators (sent, delivered, read) for better user experience.