Skip to main content

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

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

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>
  );
}

3. Message Input with Typing Indicators

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

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