Chat Application
The Chat application is a real-time messaging system that enables direct communication between users. It features a Socket.IO-based real-time connection with a REST API fallback.
Overview
Section titled “Overview”The Chat application consists of three main parts:
- User list (right sidebar) - with online/offline status
- Conversation list - with unread message counts
- Chat window - message display and sending
Key Features
Section titled “Key Features”- Real-time messaging with Socket.IO
- Online/offline status tracking
- Typing indicator
- Unread message counting
- Automatic fallback to REST API (dev mode)
- Toast notifications for new messages
- Automatic conversation sorting (newest first)
File Structure
Section titled “File Structure”apps/chat/├── index.svelte # Main layout (sidebar + conversations + chat window)├── chat.remote.ts # Server actions (messages, conversations)├── components/│ ├── UserList.svelte # User list with online/offline grouping│ ├── ConversationList.svelte # Conversation list│ └── ChatWindow.svelte # Message display and sending└── stores/ └── chatStore.svelte.ts # Chat state management and Socket.IO connectionServer Actions
Section titled “Server Actions”chat.remote.ts
Section titled “chat.remote.ts”The Chat application defines 8 server actions:
1. getChatUsers (query)
Section titled “1. getChatUsers (query)”Returns all users (except the current user) for starting a chat.
const result = await getChatUsers();// { success: true, users: ChatUser[] }2. getConversations (query)
Section titled “2. getConversations (query)”Fetches all conversations for the user with the last message and unread count.
const result = await getConversations();// { success: true, conversations: ConversationWithLastMessage[] }3. getMessages (command)
Section titled “3. getMessages (command)”Fetches messages for a conversation with pagination.
const result = await getMessages({ conversationId: 1, limit: 50, // optional, default: 50 offset: 0 // optional, default: 0});// { success: true, messages: MessageWithSender[] }Validation:
conversationId: minimum 1limit: between 1-100offset: minimum 0- Checks that the user is a member of the conversation
4. sendMessage (command)
Section titled “4. sendMessage (command)”Send a new message to a user.
const result = await sendMessage({ recipientId: 2, content: "Hello!"});// { success: true, message: MessageWithSender, conversationId: number }Validation:
recipientId: minimum 1content: between 1-5000 characters
How it works:
- Gets or creates the conversation
- Saves the message to the database
- Returns the message with sender data
5. markMessagesAsRead (command)
Section titled “5. markMessagesAsRead (command)”Mark conversation messages as read.
const result = await markMessagesAsRead({ conversationId: 1});// { success: true }6. getUnreadCount (query)
Section titled “6. getUnreadCount (query)”Get the total number of unread messages.
const result = await getUnreadCount();// { success: true, count: number }7. getCurrentUserId (query)
Section titled “7. getCurrentUserId (query)”Get the current user’s ID.
const result = await getCurrentUserId();// { success: true, userId: number }8. getOrCreateConversation (command)
Section titled “8. getOrCreateConversation (command)”Get or create a conversation with a user.
const result = await getOrCreateConversation({ otherUserId: 2});// { success: true, conversationId: number }ChatStore
Section titled “ChatStore”chatStore.svelte.ts manages the chat state and Socket.IO connection.
interface ChatState { conversations: ConversationWithLastMessage[]; activeConversationId: number | null; messages: MessageWithSender[]; unreadCount: number; isConnected: boolean; // Socket.IO connection state onlineUsers: Set<number>; // Online user IDs typingUsers: Map<number, boolean>; // Typing users per conversation}Key Methods
Section titled “Key Methods”connect(userId: number)
Section titled “connect(userId: number)”Initialize Socket.IO connection and set up event listeners.
const chatStore = getChatStore();await chatStore.connect(userId);Socket.IO events:
chat:new-message- New message receivedchat:user-online- User came onlinechat:user-offline- User went offlinechat:online-users- List of online userschat:user-typing- Typing indicator
Fallback behavior:
- If Socket.IO is unavailable, polls every 10 seconds
- In dev mode, automatically uses polling
disconnect()
Section titled “disconnect()”Disconnect Socket.IO and stop polling.
chatStore.disconnect();loadConversations()
Section titled “loadConversations()”Reload conversations from the API.
await chatStore.loadConversations();loadMessages(conversationId: number)
Section titled “loadMessages(conversationId: number)”Load messages for a conversation and make it active.
await chatStore.loadMessages(1);Side effects:
- Sets
activeConversationId - Automatically marks messages as read
- Updates unread count
sendMessage(recipientId: number, content: string)
Section titled “sendMessage(recipientId: number, content: string)”Send a message via Socket.IO.
const result = await chatStore.sendMessage(2, "Hello!");How it works:
- Calls the
sendMessageserver action - Sends via Socket.IO (
chat:send-messageevent) - Adds the message to local state (if active conversation)
- Updates the conversation list
markAsRead(conversationId: number)
Section titled “markAsRead(conversationId: number)”Mark messages as read.
await chatStore.markAsRead(1);sendTypingIndicator(recipientId, conversationId, isTyping)
Section titled “sendTypingIndicator(recipientId, conversationId, isTyping)”Send typing indicator via Socket.IO.
chatStore.sendTypingIndicator(2, 1, true); // Start typingchatStore.sendTypingIndicator(2, 1, false); // Stop typingisUserOnline(userId: number): boolean
Section titled “isUserOnline(userId: number): boolean”Check if a user is online.
if (chatStore.isUserOnline(2)) { console.log('User is online');}isUserTyping(conversationId: number): boolean
Section titled “isUserTyping(conversationId: number): boolean”Check if someone is typing in a conversation.
if (chatStore.isUserTyping(1)) { console.log('Other user is typing...');}Components
Section titled “Components”UserList.svelte
Section titled “UserList.svelte”User list with online/offline grouping and search.
Features:
- Search by name and username
- Collapsible online/offline groups
- Status indicator (green/grey)
- Click to start a conversation
Usage:
<UserList />ConversationList.svelte
Section titled “ConversationList.svelte”Conversation list with last message and unread count.
Features:
- Automatic sorting (newest first)
- Unread message badge
- Active conversation highlight
- Refresh button
- Relative timestamps (e.g. “2 minutes ago”)
Usage:
<ConversationList />ChatWindow.svelte
Section titled “ChatWindow.svelte”Message display and sending.
Features:
- Messages grouped by sender
- Avatar display
- Typing indicator with animation
- Send with Enter key
- Auto-scroll to new messages
- Empty state handling
Usage:
<ChatWindow currentUserId={userId} />Props:
currentUserId: Current user’s ID (to distinguish messages)
Socket.IO Integration
Section titled “Socket.IO Integration”Server Side
Section titled “Server Side”The Socket.IO server is configured in server.js (Express + Socket.IO).
Events (server → client):
chat:new-message- New message receivedchat:user-online- User came onlinechat:user-offline- User went offlinechat:online-users- List of online userschat:user-typing- Typing indicator
Events (client → server):
register- Register user (userId)chat:send-message- Send messagechat:mark-read- Mark messages as readchat:typing- Send typing indicator
Client Side
Section titled “Client Side”ChatStore automatically manages the Socket.IO connection:
// Connectconst chatStore = getChatStore();await chatStore.connect(userId);
// Automatic reconnection// - Infinite retries// - 1-5 second delay// - WebSocket + polling fallbackToast Notifications
Section titled “Toast Notifications”When a new message arrives (if not in the active conversation):
toast.info(senderName, { description: messagePreview, duration: 5000, action: { label: 'Open', onClick: () => openMessageInChat(conversationId) }});How it works:
- Dynamically imports
svelte-sonnertoast - Shows the sender’s name and message preview
- “Open” button opens the conversation
Database Schema
Section titled “Database Schema”conversations table
Section titled “conversations table”{ id: number; participant1Id: number; participant2Id: number; lastMessageAt: Date | null; createdAt: Date;}messages table
Section titled “messages table”{ id: number; conversationId: number; senderId: number; content: string; isRead: boolean; readAt: Date | null; sentAt: Date;}ChatRepository
Section titled “ChatRepository”chatRepository.ts handles database operations.
Key Methods
Section titled “Key Methods”getOrCreateConversation(userId1, userId2)
Section titled “getOrCreateConversation(userId1, userId2)”Get or create a conversation between two users.
const conversation = await chatRepository.getOrCreateConversation(1, 2);How it works:
- Checks both directions (participant1 ↔ participant2)
- Creates if it doesn’t exist
getUserConversations(userId)
Section titled “getUserConversations(userId)”Get all conversations for a user.
const conversations = await chatRepository.getUserConversations(1);Returns:
- Conversation data
- Other user’s name and image
- Last message
- Unread message count
getConversationMessages(conversationId, limit, offset)
Section titled “getConversationMessages(conversationId, limit, offset)”Get messages for a conversation.
const messages = await chatRepository.getConversationMessages(1, 50, 0);How it works:
- Paginated (limit + offset)
- Chronological order (oldest → newest)
- Sender name and image attached
sendMessage(conversationId, senderId, content)
Section titled “sendMessage(conversationId, senderId, content)”Save a new message.
const message = await chatRepository.sendMessage(1, 2, "Hello!");Side effects:
- Updates the conversation’s
lastMessageAtfield
markMessagesAsRead(conversationId, userId)
Section titled “markMessagesAsRead(conversationId, userId)”Mark conversation messages as read.
await chatRepository.markMessagesAsRead(1, 2);How it works:
- Only marks the other user’s messages as read
- Sets
isReadandreadAtfields
getUserUnreadCount(userId)
Section titled “getUserUnreadCount(userId)”Get the total number of unread messages.
const count = await chatRepository.getUserUnreadCount(1);Usage Examples
Section titled “Usage Examples”Starting the Chat Application
Section titled “Starting the Chat Application”import { getChatStore } from '$apps/chat/stores/chatStore.svelte';import { getCurrentUserId } from '$apps/chat/chat.remote';
// Get user IDconst result = await getCurrentUserId();if (result.success && result.userId) { // Initialize ChatStore const chatStore = getChatStore(); await chatStore.connect(result.userId);}Starting a New Conversation
Section titled “Starting a New Conversation”import { getChatStore } from '$apps/chat/stores/chatStore.svelte';import { getOrCreateConversation } from '$apps/chat/chat.remote';
const chatStore = getChatStore();
// Create conversationconst result = await getOrCreateConversation({ otherUserId: 2 });
if (result.success && result.conversationId) { // Refresh conversations await chatStore.loadConversations();
// Open conversation await chatStore.loadMessages(result.conversationId);}Sending a Message
Section titled “Sending a Message”import { getChatStore } from '$apps/chat/stores/chatStore.svelte';
const chatStore = getChatStore();
// Send messageconst result = await chatStore.sendMessage(2, "Hello, how are you?");
if (result.success) { console.log('Message sent');}Typing Indicator
Section titled “Typing Indicator”import { getChatStore } from '$apps/chat/stores/chatStore.svelte';
const chatStore = getChatStore();let typingTimeout: ReturnType<typeof setTimeout> | null = null;
function handleInput(recipientId: number, conversationId: number) { // Start typing chatStore.sendTypingIndicator(recipientId, conversationId, true);
// Clear timeout if (typingTimeout) clearTimeout(typingTimeout);
// Stop typing after 3 seconds typingTimeout = setTimeout(() => { chatStore.sendTypingIndicator(recipientId, conversationId, false); }, 3000);}Translations
Section titled “Translations”Chat application translations are in the translations.chat namespace:
-- packages/database/src/seeds/translations/chat.tsINSERT INTO translations (namespace, key, locale, value) VALUES ('chat', 'title', 'hu', 'Chat'), ('chat', 'title', 'en', 'Chat'), ('chat', 'users', 'hu', 'Felhasználók'), ('chat', 'users', 'en', 'Users'), ('chat', 'conversations', 'hu', 'Beszélgetések'), ('chat', 'conversations', 'en', 'Conversations'), -- ...Usage in component:
<script> import { I18nProvider } from '$lib/i18n/components';</script>
<I18nProvider namespaces={['chat', 'common']}> <!-- Chat components --></I18nProvider>Best Practices
Section titled “Best Practices”- Socket.IO connection management: Always call
disconnect()when the application closes - Typing indicator: Use a timeout to avoid sending continuous events
- Message pagination: Implement “load more” for large conversations
- Offline operation: The fallback polling ensures it works in dev mode too
- Toast notifications: Only show when the message is not in the active conversation
- Unread counting: Automatically updated with Socket.IO events
- Avatar images: Use
referrerpolicy="no-referrer"andcrossorigin="anonymous"attributes - Message grouping: Only show avatar when there is a new sender
Troubleshooting
Section titled “Troubleshooting”Socket.IO Not Connecting
Section titled “Socket.IO Not Connecting”Problem: isConnected always stays false.
Solution:
- Check that the Socket.IO server is running (
server.js) - Check the browser console for Socket.IO errors
- In dev mode, polling fallback activates automatically
Messages Not Showing
Section titled “Messages Not Showing”Problem: New message doesn’t appear in the chat window.
Solution:
- Check that the active conversation ID is correct
- Check if the
chat:new-messageevent is arriving (DevTools Network tab) - Check the
currentUserIdprop on theChatWindowcomponent
Typing Indicator Not Working
Section titled “Typing Indicator Not Working”Problem: The typing indicator doesn’t appear.
Solution:
- Check that Socket.IO is connected
- Check the sending and receiving of the
chat:typingevent - Check the
typingUsersMap in the store
Online Status Not Updating
Section titled “Online Status Not Updating”Problem: Users always appear offline.
Solution:
- Check the
chat:online-usersevent reception - Check the
onlineUsersSet in the store - In dev mode, polling updates the online status