Messaging System
Overview
The Messaging System provides threaded, multi-recipient communication with broadcast support, file attachments, and activity tracking. Users can send direct messages, group messages to teams/projects, or broadcast to all users.
The Messaging panel with threaded conversations, recipients, and message history
Key Features
- Threaded Messaging - Organize conversations by topic
- Multiple Recipients - Individual, team, project, or broadcast
- File Attachments - Share files in messages
- Read Receipts - Track message reading via last_read_at
- Message Search - Search across threads and content
- Soft Delete - Participants can leave conversations
- Activity Tracking - See who replied and when
- Multi-language - Support for message body translations
Core Data Structures
interface Thread {
id: number;
subject: string;
is_broadcast: boolean;
creator_id: number;
created_at: string;
updated_at: string;
latest_message?: Message;
other_participants?: User[];
has_unread?: boolean;
messages?: Message[];
participants?: ThreadParticipant[];
}
interface Message {
id: number;
thread_id: number;
sender_id: number;
body: string;
created_at: string;
updated_at: string;
sender: User;
attachments?: Attachment[];
is_edited?: boolean;
}
interface Attachment {
id: number;
message_id: number;
file_name: string;
file_path: string;
file_size: number;
mime_type: string;
created_at: string;
}
interface ThreadParticipant {
id: number;
thread_id: number;
user_id: number;
last_read_at?: string;
deleted_at?: string; // Soft delete
role: string;
}
Recipient Types
Individual Messaging
Direct message between two users:
const thread = {
subject: 'Project Discussion',
is_broadcast: false,
recipient_type: 'individual',
recipient_id: targetUserId
};
Project Messaging
Message to all project members:
const thread = {
subject: 'Project Update',
recipient_type: 'project',
project_id: projectId
};
// All project members receive
Team Messaging
Message to specific team within project:
const thread = {
subject: 'Team Standup',
recipient_type: 'team',
team_id: teamId
};
Broadcast Messaging
Message to all users (admin only):
const thread = {
subject: 'System Announcement',
is_broadcast: true,
recipient_type: 'broadcast'
};
// Permission check: requires broadcast capability
Read Receipts
Track when participants read messages:
interface ThreadParticipant {
last_read_at: string | null; // ISO timestamp
}
// Mark as read
const markAsRead = async (threadId: number) => {
await fetch(`/api/threads/${threadId}/mark-read`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
};
// Unread count
const unreadCount = threads.filter(t => !t.last_read_at).length;
// Who has read
const hasRead = (thread: Thread, userId: number) => {
const participant = thread.participants.find(p => p.user_id === userId);
return participant?.last_read_at &&
new Date(participant.last_read_at) > new Date(thread.updated_at);
};
Message Features
Reply to Thread
const handleReply = async (threadId: number, body: string) => {
const response = await fetch(`/api/threads/${threadId}/messages`, {
method: 'POST',
body: JSON.stringify({ body }),
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return await response.json();
};
File Attachments
Attachments are a premium feature for patron users:
interface AttachmentAccess {
has_access: boolean;
is_patron: boolean;
is_system?: boolean;
subscription_ends_at?: string;
user_credits?: number;
}
// Check before uploading
const checkAttachmentAccess = async () => {
const response = await fetch('/api/messages/attachment-access', {
headers: { 'Authorization': `Bearer ${token}` }
});
return await response.json();
};
// Upload with message
const sendWithAttachments = async (threadId: number, body: string, files: File[]) => {
const formData = new FormData();
formData.append('body', body);
files.forEach((file, i) => {
formData.append(`attachments[${i}]`, file);
});
await fetch(`/api/threads/${threadId}/messages`, {
method: 'POST',
body: formData,
headers: { 'Authorization': `Bearer ${token}` }
});
};
Edit Message
const handleEditMessage = async (messageId: number, newBody: string) => {
await fetch(`/api/messages/${messageId}`, {
method: 'PUT',
body: JSON.stringify({ body: newBody }),
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
};
Delete Message
Soft delete removes message from participant's view:
const handleDeleteMessage = async (messageId: number) => {
await fetch(`/api/messages/${messageId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
};
Thread Management
Create Thread
const createThread = async (subject: string, recipientType: string, recipientId?: number) => {
const response = await fetch('/api/threads', {
method: 'POST',
body: JSON.stringify({
subject,
recipient_type: recipientType,
recipient_id: recipientId
}),
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return await response.json();
};
Leave Thread
Soft delete participant from thread (thread remains for others):
const leaveThread = async (threadId: number) => {
await fetch(`/api/threads/${threadId}/leave`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
};
Archive Thread
Mark thread as archived for organization:
const archiveThread = async (threadId: number) => {
await fetch(`/api/threads/${threadId}`, {
method: 'PUT',
body: JSON.stringify({ is_archived: true }),
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
};
Search & Filtering
Search Threads
const searchThreads = async (query: string) => {
const response = await fetch(`/api/threads/search?q=${encodeURIComponent(query)}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
return await response.json();
};
// Searches: subject, message body, sender name
Filter Options
// Unread only
threads.filter(t => t.has_unread);
// From specific user
threads.filter(t => t.latest_message?.sender_id === userId);
// Broadcast messages
threads.filter(t => t.is_broadcast);
// Recent threads (last 7 days)
threads.filter(t =>
new Date(t.updated_at) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
);
User Suggestions
Autocomplete Recipients
const searchUsers = async (query: string) => {
const response = await fetch(`/api/users/search?q=${encodeURIComponent(query)}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
return await response.json();
};
// Show matching users
const userSuggestions = await searchUsers('john');
// Returns: [ { id: 1, name: 'John Doe', username: 'john.doe' }, ... ]
Real-time Updates
WebSocket Subscriptions
// Subscribe to thread
const channel = getEcho().channel(`thread.${threadId}`);
channel.listen('MessageSent', (data) => {
addMessage(data.message);
markAsRead(threadId);
});
channel.listen('UserTyping', (data) => {
showTypingIndicator(data.user.name);
});
channel.listen('MessageDeleted', (data) => {
removeMessage(data.message_id);
});
API Endpoints
# Threads
GET /api/threads # List user threads
POST /api/threads # Create thread
GET /api/threads/{id} # Get thread
PUT /api/threads/{id} # Update thread
POST /api/threads/{id}/leave # Leave thread
POST /api/threads/{id}/mark-read # Mark as read
# Messages
GET /api/threads/{id}/messages # Get messages (paginated)
POST /api/threads/{id}/messages # Send message
PUT /api/messages/{id} # Edit message
DELETE /api/messages/{id} # Delete message
# Attachments
GET /api/messages/attachment-access # Check attachment permission
DELETE /api/attachments/{id} # Delete attachment
# Search
GET /api/threads/search?q=... # Search threads
GET /api/users/search?q=... # Search users
Permission Model
Message Creator
- Edit own messages
- View all messages in thread
- Delete message (soft delete)
Thread Participants
- Read messages
- Reply to thread
- See participant list
- Mark as read
- Leave thread
Non-Participants
- Cannot see thread
- Cannot join unless invited
Broadcast Recipient
- Read-only access
- No reply option (may change)
Best Practices
- Use Clear Subjects - Help find conversations later
- Keep Threads Focused - One topic per thread
- Search Before Creating - Avoid duplicate threads
- Check Attachments - Know feature limitations
- Archive Old Threads - Keep inbox organized
- Use Appropriate Recipients - Don't over-broadcast
- Respond Promptly - Don't leave colleagues waiting
- Reference Messages - Quote when necessary
Common Use Cases
Project Update
Recipients: Project Subject: "Weekly Progress Report" Message: Summary of accomplishments and blockers
Team Decision
Recipients: Team Subject: "Technology Stack Recommendation" Attachments: Comparison document
One-on-One
Recipients: Individual User Subject: Discussion topic Private conversation with colleague
System Announcement
Recipients: Broadcast (admin only) Subject: "Scheduled Maintenance Notice" Inform all users of important updates
Troubleshooting
Messages Not Appearing
- Check WebSocket connection
- Verify participant status
- Refresh page
Attachments Not Uploading
- Check patron/credit status
- Verify file size limits
- Confirm file type allowed
Unread Status Not Updating
- Check read receipt sync
- Verify last_read_at timestamp
- Refresh thread
Next Steps
- Kanban Board - Task management
- Team Management - Manage users
- Project Settings - Configure communication