Chat
Core room-based chat system with DMs, presence tracking, AI bots, and real-time collaboration.
Overview
The chat module is the foundation of the doh_chat suite. It provides a full-featured chat application with public/private rooms, direct messages, real-time presence, file sharing, collaborative documents, AI bot conversations, and video/audio calls. The server handles all persistence (SQLite via AsyncDataforge), permission enforcement, and socket event routing. The client renders a composable UI with sidebar navigation, message lists, settings panels, and embedded content tiles.
Architecture
chat.doh.yaml
├── chat_url_utils (shared)
├── ai_services (nodejs)
├── chat_server (nodejs) ← routes, socket events, permissions, DB
├── chat_files_server (nodejs)
├── chat_push_server (nodejs)
├── chat_calls_server (nodejs)
├── collab_docs (shared)
├── collab_richtext (shared)
├── user_avatar (browser)
├── chat_client (browser) ← full UI (ChatApp + 40 patterns)
├── chat_files_client (browser)
├── chat_push_client (browser)
└── chat_calls_client (browser)
Depends on: express_router, user_host (auth), db_dataforge
Depended on by: chat_embed, issues
Features
- Public lobby room (always available, auto-join)
- Room-based chat with permission-gated creation
- Direct messages between users
- AI bot conversations with multi-provider support
- Real-time presence tracking (online/offline indicators)
- Message history with edit/delete and provenance tracking
- Typing indicators
- File attachments (via
chat_files)
- WebRTC audio/video calls (via
chat_calls)
- Collaborative code documents (via
collab_docs)
- Collaborative rich text documents (via
collab_richtext)
- Push notifications for iOS (via
chat_push)
- Browser extension tool integration
- Contact/connection request system with blocking
- Sidebar folder organization with drag-and-drop
- Email invitations
- Server log streaming for admins
- YouTube video synchronization
- Scroll position synchronization
- Admin maintenance tools (delete rooms, token usage)
File Structure
| File |
Purpose |
chat.doh.yaml |
Outer Package definition |
chat_server.js |
Server routes, socket events, permissions, DB setup (~5200 lines) |
chat_client.js |
Full client UI — 40 Doh Patterns (~12,400 lines) |
chat_calls_server.js |
WebRTC call signaling server (~400 lines) |
chat_calls_client.js |
Call manager and call UI patterns (~970 lines) |
chat_push_server.js |
Push notification registration and sending (~385 lines) |
chat_push_client.js |
Capacitor push notification setup (~200 lines) |
chat_url_utils.js |
URL parsing utilities (~100 lines) |
chat.css |
Main chat styles |
chat_calls.css |
Call UI styles |
BROWSER_TOOLS.md |
Browser extension tool registry docs |
Configuration
# pod.yaml
chat:
lobby_name: "General Chat"
max_message_length: 5000
attachment_storage_enabled: true
user_key_secret: "your-secret-key" # AES key for encrypting user AI keys
models: {} # AI model configuration overrides
providers: {} # Provider key map overrides
Database Schema
All tables are in the chat database (SQLite via AsyncDataforge). All are Idea tables (id TEXT, data TEXT JSON blob) unless noted.
| Table |
Purpose |
chat.rooms |
Room metadata (name, type, members, settings, created_by) |
chat.messages |
Message history (sender, content, timestamps, provenance) |
chat.room_members |
Membership records (username, role, joined_at, unread_count) |
chat.contacts |
User contact lists |
chat.connections |
Established user-to-user connections |
chat.connection_requests |
Pending connection requests |
chat.blocked_users |
Blocked user records |
chat.email_invites |
Email-based room invitations |
chat.sidebar_layouts |
Saved sidebar panel layouts per user |
chat.trace |
Browser navigation trace/history entries |
chat.bots |
Bot configurations and metadata |
chat.user_ai_keys |
Encrypted user API keys (AES-256-GCM) |
chat.pro_interest |
Doh Pro subscription interest registrations |
chat.token_usage |
AI token usage tracking per user/room/model |
Constants: LOBBY_ID = 'lobby'
Permissions
Contexts
| Context |
Condition |
chat_room |
ctx?.roomId exists |
server_logs |
Always true |
chat_maintenance |
Always true |
dohpro |
Always true |
Groups
| Group |
Type |
Permissions |
Use Case |
chat_room_creator |
Assignable |
create:chat_room |
Users who can create rooms |
log_viewer |
Assignable |
view:server_logs |
Users who can stream server logs |
chat_admin |
Assignable |
admin:chat_maintenance |
Admin maintenance (delete rooms, view all token usage) |
dohpro_subscriber |
Assignable |
is:dohpro |
Doh Pro subscribers (access managed AI keys) |
Granting Permissions
doh users add-group user@example.com chat_room_creator
doh users add-group user@example.com chat_admin
REST API Reference
All routes are prefixed with /api/chat/ unless noted. Auth = [Users.requireAuth]. Nickname = user must have set chat_name.
Profile & Settings
| Method |
Path |
Auth |
Description |
| POST |
/api/chat/nickname |
Yes |
Set chat display name |
| GET/POST |
/api/chat/avatar |
Yes |
Get/set avatar preferences |
| GET |
/api/chat/me |
Yes |
Get current user profile |
POST /api/chat/nickname
// Request
{ chat_name: string } // 2-32 chars
// Response
{ success: true, chat_name, user: {...} }
// Errors: 400 (length), 409 (duplicate)
POST /api/chat/avatar
// Request
{ avatar_salt?: number, avatar_style?: string }
// Styles: 'initials', 'bottts-neutral', 'pixel-art', 'identicon', 'rings', 'shapes'
// Response
{ success: true, avatar_salt, avatar_style }
Rooms — Discovery
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/lobby |
Yes + Nick |
Get lobby room and members |
| GET |
/api/chat/rooms/public |
Yes + Nick |
List all public rooms |
| GET |
/api/chat/rooms |
Yes + Nick |
List rooms user is a member of |
| GET |
/api/chat/rooms/:id |
Yes + Nick |
Get single room details + members |
| GET |
/api/chat/rooms/:id/messages |
Optional |
Fetch message history (paginated) |
GET /api/chat/rooms/:id/messages
// Request (query/body)
{ limit?: number, skip?: number }
// Response
{ messages: [{ id, sender, sender_name, content, created_at, edited_at, deleted, provenance, ... }] }
Rooms — Management
| Method |
Path |
Auth |
Permission |
Description |
| POST |
/api/chat/rooms/create |
Yes + Nick |
create:chat_room |
Create a room |
| PATCH |
/api/chat/rooms/:id |
Yes |
Room admin |
Edit room settings |
| DELETE |
/api/chat/rooms/:id |
Yes |
Room admin |
Delete room |
| POST |
/api/chat/rooms/:id/delete |
Yes |
Room creator |
Delete room (explicit) |
POST /api/chat/rooms/create
// Request
{ name: string, members?: string[], type?: 'public'|'private'|'dm'|'bot'|'issue', is_public?: boolean }
// Response
{ room: { id, name, type, members, created_by, created_at, ... } }
// Broadcast: chat:room_updated to all members
Room Membership
| Method |
Path |
Auth |
Role |
Description |
| POST |
/api/chat/rooms/:id/join |
Yes + Nick |
-- |
Join public/auto_membership room |
| POST |
/api/chat/rooms/:id/leave |
Yes |
-- |
Leave room (can't leave lobby) |
| POST |
/api/chat/rooms/:id/members |
Yes |
Admin |
Add member |
| PATCH |
/api/chat/rooms/:id/members/:username |
Yes |
Admin |
Change member role |
| DELETE |
/api/chat/rooms/:id/members/:username |
Yes |
Admin |
Remove member |
| GET |
/api/chat/rooms/:id/members |
Yes |
-- |
List members |
POST /api/chat/rooms/:id/members
// Request
{ username: string, role?: 'member'|'admin' }
// Response
{ success: true, member: { username, role, joined_at, ... } }
// Broadcast: chat:member_added
Direct Messages
| Method |
Path |
Auth |
Description |
| POST |
/api/chat/dm/:username |
Yes + Nick |
Create/get DM room with user |
| POST |
/api/chat/dm/bot/:bot_id |
Yes |
Create/get DM room with bot |
POST /api/chat/dm/:username
// Response
{ room: { id, type: 'dm', members: [user, target], partner_chat_name, ... } }
// Broadcast (if new): chat:room_created to both users
AI & Bots
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/ai/models |
Yes |
List available AI models (filtered by keys/pro) |
| GET |
/api/chat/ai/keys |
Yes |
Get user's key status per provider (masked) |
| POST |
/api/chat/ai/keys/save |
Yes |
Save encrypted AI key |
| POST |
/api/chat/ai/keys/delete |
Yes |
Delete AI key |
| POST |
/api/chat/ai/keys/reveal |
Yes |
Decrypt and return full key |
| GET |
/api/chat/bots |
Yes |
List user's bots |
| POST |
/api/chat/bots/create |
Yes |
Create a bot |
| GET |
/api/chat/bots/:id |
Yes |
Get bot details |
| POST |
/api/chat/bots/:id/update |
Yes |
Update bot config |
| POST |
/api/chat/bots/:id/delete |
Yes |
Delete bot |
GET /api/chat/ai/models
// Response
{
models: [{
nickname: string, name: string, description: string, provider: string,
accessible: boolean, access_source: 'user_key'|'doh_pro'|null,
reason: 'no_key'|null
}],
is_doh_pro: boolean
}
Bot Room Operations
| Method |
Path |
Auth |
Description |
| POST |
/api/chat/rooms/:id/clear-context |
Yes |
Clear bot conversation context |
| POST |
/api/chat/rooms/:id/compress |
Yes |
Summarize and compress context |
| POST |
/api/chat/rooms/:id/clone |
Yes |
Clone bot room with empty history |
| POST |
/api/chat/rooms/:id/model |
Yes |
Change bot's AI model |
Token Usage
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/token-usage |
Yes |
User's aggregated token usage |
| GET |
/api/chat/rooms/:id/token-usage |
Yes |
Per-room token usage |
| GET |
/api/chat/admin/token-usage |
Admin |
All users' token usage |
GET /api/chat/token-usage
// Response
{
models: { "model_name": { input_tokens, output_tokens, cost, calls } },
total: { input_tokens, output_tokens, cost, calls }
}
Contacts & Connections
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/contacts |
Yes |
Get contact list with online status |
| POST |
/api/chat/contacts/:username |
Yes |
Add to contacts |
| DELETE |
/api/chat/contacts/:username |
Yes |
Remove from contacts |
| POST |
/api/chat/connections/request/:username |
Yes |
Send connection request |
| GET |
/api/chat/connections/requests/pending |
Yes |
Incoming requests |
| GET |
/api/chat/connections/requests/sent |
Yes |
Outgoing requests |
| POST |
/api/chat/connections/accept/:requester |
Yes |
Accept request |
| POST |
/api/chat/connections/decline/:requester |
Yes |
Decline request |
| GET |
/api/chat/connections |
Yes |
All connections |
| GET |
/api/chat/connections/status/:username |
Yes |
Check status with user |
| DELETE |
/api/chat/connections/remove/:username |
Yes |
Remove connection |
Blocking
| Method |
Path |
Auth |
Description |
| POST |
/api/chat/block/:username |
Yes |
Block user |
| POST |
/api/chat/unblock/:username |
Yes |
Unblock user |
| GET |
/api/chat/blocked |
Yes |
List blocked users |
Presence & Users
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/presence |
Yes + Nick |
Online users list |
| GET |
/api/chat/users |
Yes + Nick |
Paginated user discovery |
| GET |
/api/chat/users/by-email |
Yes |
Find user by email |
Sidebar & Links
| Method |
Path |
Auth |
Description |
| GET |
/api/chat/sidebar-layout |
Yes |
Get saved sidebar layout |
| POST |
/api/chat/sidebar-layout/save |
Yes |
Save sidebar layout |
| POST |
/api/chat/sidebar-layout/reset |
Yes |
Reset to default |
| GET |
/api/chat/links/user |
Yes + Nick |
Get browser trace links |
Invites & Pro
| Method |
Path |
Auth |
Description |
| POST |
/api/chat/invite-by-email |
Yes |
Send room invite via email |
| POST |
/api/chat/ai/pro-interest |
Yes |
Register for Doh Pro |
| GET |
/api/chat/ai/pro-interest/check |
Yes |
Check Pro registration |
Admin
| Method |
Path |
Auth |
Permission |
Description |
| POST |
/api/chat/admin/delete-all-rooms |
Yes |
admin:chat_maintenance |
Delete all rooms except lobby |
| POST |
/api/chat/admin/delete-empty-rooms |
Yes |
admin:chat_maintenance |
Delete rooms with < 3 messages |
// Response (both)
{ success: true, deleted_count: number }
Page Route
| Method |
Path |
Auth |
Description |
| GET |
/chat |
Yes |
Full chat application page |
Socket Events Reference
All events use wrapSocketHandler for consistent error handling. Auth comes from socket.user (authenticated) or socket.guestUser (guest with nickname).
Connection & Identity
| Event |
Direction |
Payload |
Response/Broadcast |
chat:set_guest_identity |
Client → Server |
{ chat_name, guest_id? } |
cb({ success, guest_user }) |
Room Lifecycle
| Event |
Direction |
Payload |
Response/Broadcast |
chat:join |
Client → Server |
{ room_id } |
cb({ success }) |
chat:leave |
Client → Server |
{ room_id } |
cb({ success }) |
chat:user_joined |
Server → Room |
{ username, chat_name, online } |
Broadcast to room |
chat:room_peer_joined |
Server → Room |
{ username, chat_name } |
Broadcast to room |
Messaging
| Event |
Direction |
Payload |
Response/Broadcast |
chat:message |
Client → Server |
{ room_id, content, scroll_y?, scroll_percent?, page_url? } |
cb({ success, message }) |
chat:edit |
Client → Server |
{ message_id, content } |
cb({ success }) |
chat:delete |
Client → Server |
{ message_id } |
cb({ success }) |
chat:notey_edit |
Client → Server |
{ message_id, content } |
cb({ success }) |
chat:read |
Client → Server |
{ room_id, message_id? } |
cb({ success }) |
chat:reorder |
Client → Server |
{ room_id, ordering: string[] } |
cb({ success }) |
chat:message_received |
Server → Room |
{ room_id, message } |
Broadcast |
chat:message_edited |
Server → Room |
{ room_id, message_id, content, edited_at } |
Broadcast |
chat:message_deleted |
Server → Room |
{ room_id, message_id } |
Broadcast |
chat:unread_update |
Server → User |
{ room_id, unread_count } |
Per-user |
chat:messages_reordered |
Server → Room |
{ room_id, ordering } |
Broadcast |
Message object shape (returned in chat:message callback and chat:message_received):
{
id: string, // UUID
room_id: string,
sender: string, // username
sender_name: string, // chat_name
avatar_salt: number,
avatar_style: string,
content: string,
created_at: number, // timestamp
edited_at: number | null,
deleted: boolean,
is_guest: boolean,
original_sequence_number: number,
adhoc_sequence_number: number | null,
provenance: [{ action: 'created'|'edited'|'deleted', user, name, at }]
}
Typing
| Event |
Direction |
Payload |
Response/Broadcast |
chat:typing |
Client → Server |
{ room_id, typing: boolean } |
cb({ success }) |
chat:user_typing |
Server → Room |
{ room_id, username, chat_name, typing } |
Broadcast (excludes sender) |
Presence
| Event |
Direction |
Payload |
Broadcast |
chat:presence |
Server → Lobby |
{ username, chat_name, online } |
All lobby users |
Embedded Documents
| Event |
Direction |
Payload |
Response/Broadcast |
chat:update_doc_size |
Client → Server |
{ message_id, doc_id, width, height, isLive? } |
cb({ success }) |
chat:update_doc_open_state |
Client → Server |
{ message_id, doc_id, isOpen } |
cb({ success }) |
chat:doc_size_updated |
Server → Room |
{ message_id, doc_id, width, height } |
Broadcast |
chat:doc_open_state_updated |
Server → Room |
{ message_id, doc_id, isOpen } |
Broadcast |
Browser Panels
| Event |
Direction |
Payload |
Response/Broadcast |
chat:browser_panel:open |
Client → Server |
{ room_id, panel_id, url, left, top, width, height, owner } |
cb({ success, panel_id }) |
chat:browser_panel:url |
Client → Server |
{ room_id, panel_id, url } |
cb({ success }) |
chat:browser_panel:state |
Client → Server |
{ room_id, panel_id, left, top, width, height } |
cb({ success }) |
chat:browser_panel:close |
Client → Server |
{ room_id, panel_id } |
cb({ success }) |
chat:browser_panel:opened |
Server → Room |
Same as open payload |
Broadcast (excludes sender) |
chat:browser_panel:url_update |
Server → Room |
{ panel_id, url } |
Broadcast (excludes sender) |
chat:browser_panel:state_update |
Server → Room |
{ panel_id, left, top, width, height } |
Broadcast (excludes sender) |
Browser Tool Integration
| Event |
Direction |
Payload |
chat:browser_tool_request |
Server → Room |
{ room_id, request_id, tool_name, params, timeout } |
chat:browser_tool_result |
Client → Server |
{ room_id, request_id, result } |
See BROWSER_TOOLS.md for available tools.
Trace / Browser History
| Event |
Direction |
Payload |
Response/Broadcast |
chat:trace_update |
Client → Server |
{ room_id, url, title, favicon_domain?, scroll_anchor? } |
cb({ success }) |
chat:trace_sync |
Client → Server |
{ room_id } |
cb({ entries: [...] }) |
chat:trace_delete |
Client → Server |
{ room_id, entry_id } |
cb({ success }) |
chat:trace_rename |
Client → Server |
{ room_id, entry_id, nickname, url? } |
cb({ success }) |
chat:trace_updated |
Server → Room |
{ room_id, entry } |
Broadcast |
chat:trace_deleted |
Server → Room |
{ room_id, entry_id } |
Broadcast |
chat:trace_renamed |
Server → Room |
{ room_id, entry_id, nickname, url? } |
Broadcast |
Scroll Sync
| Event |
Direction |
Payload |
chat:scroll_sync |
Client → Server |
{ room_id, payload: { scroll_y, scroll_percent, ... } } |
chat:scroll_sync |
Server → Room |
{ room_id, from_user, payload } (excludes sender) |
YouTube Sync
| Event |
Direction |
Payload |
chat:yt_sync |
Client → Server |
{ room_id, message_id, url, action: 'play'|'pause'|'seek', time } |
chat:yt_sync |
Server → Room |
Same payload (excludes sender) |
Server Logs
| Event |
Direction |
Payload |
Notes |
chat:logs:subscribe |
Client → Server |
{ filter? } |
Requires view:server_logs |
chat:logs:unsubscribe |
Client → Server |
-- |
|
chat:logs:entry |
Server → Client |
{ timestamp, level, message, source } |
Stream |
Calls (via chat_calls_server.js)
| Event |
Direction |
Payload |
chat:call:join |
Client → Server |
{ room_id, audio: bool, video: bool } |
chat:call:leave |
Client → Server |
{ room_id } |
chat:call:toggle_audio |
Client → Server |
{ room_id, enabled } |
chat:call:toggle_video |
Client → Server |
{ room_id, enabled } |
chat:call:toggle_screen |
Client → Server |
{ room_id, enabled } |
chat:call:rtc_offer |
Client → Server |
{ room_id, target, offer } |
chat:call:rtc_answer |
Client → Server |
{ room_id, target, answer } |
chat:call:rtc_ice |
Client → Server |
{ room_id, target, candidate } |
chat:call:get_state |
Client → Server |
{ room_id } |
chat:call:state |
Server → Room |
{ room_id, participants: [...] } |
chat:call:peer_joined |
Server → Room |
{ room_id, username, audio, video } |
chat:call:peer_left |
Server → Room |
{ room_id, username } |
chat:call:media_state |
Server → Room |
{ room_id, username, audio, video, screen } |
Push Notifications (via chat_push_server.js)
| Event |
Direction |
Payload |
chat:push:register |
Client → Server |
{ token, platform } |
chat:push:unregister |
Client → Server |
{ token } |
Room/Connection Broadcasts
| Event |
Payload |
Notes |
chat:room_created |
{ room } |
Sent to new members |
chat:room_updated |
{ room } |
Sent to room members |
chat:room_deleted |
{ room_id } |
Sent to room members |
chat:member_added |
{ room_id, member } |
Sent to room |
chat:member_removed |
{ room_id, username } |
Sent to room |
chat:context_cleared |
{ room_id } |
Sent to room |
chat:sidebar_layout_updated |
{ layout } |
Sent to user's sockets |
chat:connection_request |
{ from_username, from_chat_name } |
Sent to target |
chat:connection_accepted |
{ username } |
Sent to both users |
chat:connection_removed |
{ username } |
Sent to both users |
Programmatic API
chat_server exports a Chat object for use by other modules:
Doh.Module('my_module', ['chat_server'], function(Chat) {
// Create a system room
const room = await Chat.createSystemRoom({
name: 'Issue #42',
type: 'issue',
members: ['alice', 'bob'],
adminUser: 'alice'
});
// Manage membership
await Chat.addMemberToRoom(room.id, 'charlie', 'member');
await Chat.removeMemberFromRoom(room.id, 'bob');
// Query state
const online = Chat.isUserOnline('alice'); // boolean
const rooms = Chat.getUserRooms('alice'); // Room[]
const msgs = Chat.getRecentMessages(room.id, 50); // Message[]
const isMember = Chat.isRoomMember(room.id, 'alice'); // boolean
await Chat.deleteRoom(room.id);
});
Exported Functions
| Function |
Params |
Returns |
Description |
createSystemRoom(config) |
{ name, type?, members?, created_by?, is_public?, allow_guests?, adminUser? } |
Room |
Create room programmatically |
addMemberToRoom(roomId, username, role?) |
string, string, 'member'|'admin' |
MemberRecord | null |
Add member |
removeMemberFromRoom(roomId, username) |
string, string |
void |
Remove member |
deleteRoom(roomId) |
string |
boolean |
Delete room and all data |
isUserOnline(username) |
string |
boolean |
Check presence |
trackUserOnline(socket, user) |
socket, user |
void |
Register presence |
trackUserOffline(socket) |
socket |
void |
Unregister presence |
getOnlineUsers() |
-- |
[{ username, chat_name }] |
All online users |
broadcastUserPresence(user, online) |
user, boolean |
void |
Emit presence event |
getRoomById(roomId) |
string |
Room | null |
Fetch room |
getUserRooms(username) |
string |
Room[] |
User's rooms |
getPublicRooms() |
-- |
Room[] |
All public rooms |
getRecentMessages(roomId, limit?) |
string, number |
Message[] |
Fetch messages |
isRoomMember(roomId, username) |
string, string |
boolean |
Check membership |
canParticipate(user) |
user |
boolean |
Has chat_name set |
Client Patterns
Core UI (chat_client.js)
| Pattern |
Parent |
Purpose |
ChatApp |
ChatConversation |
Top-level app: sidebar, room state, nickname setup |
ChatConversation |
html |
Abstract base: messages, input, controls, socket wiring |
ChatSidebar |
html |
Left nav: rooms, DMs, folders, issues section, user panel |
ChatMessageList |
html |
Message container with layout modes (linear/thread/grouped) |
ChatMessage |
html |
Single message: avatar, content, actions, attachments |
ChatInput |
html |
Textarea with emoji picker, file attach, link preview, auto-resize |
ChatTypingIndicator |
html |
"User is typing..." display |
ChatNicknameSetup |
html |
First-login nickname prompt |
Content Tiles
| Pattern |
Parent |
Purpose |
ChatCollabTile |
html |
Embedded collab document tile with mini editor |
ChatLogTile |
html |
Log/trace content display (URLs, stack traces) |
ChatUrlTile |
html |
Unfurled URL preview (title, description, thumbnail) |
ChatUrlOverlay |
html |
Full-page URL preview expansion |
ChatDropPopover |
html |
File drag-and-drop preview popover |
Modals & Menus
| Pattern |
Parent |
Purpose |
ChatAddMenu |
html |
Dropdown: create room, contact, folder, doc, bot |
ChatModal |
html |
Base modal dialog container |
ChatEmojiPickerModal |
html |
Emoji picker modal |
ChatAddContactModal |
html |
Add contact by username |
ChatUserPickerModal |
html |
Multi-select user picker |
ChatFolderContextMenu |
html |
Folder right-click: rename, delete |
ChatRoomContextMenu |
html |
Room right-click: rename, delete, archive |
Settings Panels
| Pattern |
Parent |
Purpose |
ChatSettings |
html |
Root settings container with tab nav |
ChatSettingsAccount |
html |
Profile, password, preferences |
ChatSettingsAppearance |
html |
Theme, font size, message layout |
ChatSettingsExtensions |
html |
Plugins management |
ChatSettingsAIKeys |
html |
AI provider API key management |
ChatSettingsUsage |
html |
Token usage analytics |
ChatSettingsAIBots |
html |
Bot definitions and config |
ChatSettingsNotifications |
html |
Sound, desktop, mute rules |
ChatSettingsAttachments |
html |
File upload limits |
ChatSettingsStorage |
html |
Local cache, export/import |
ChatSettingsCollab |
html |
Collab document settings |
ChatSettingsLinks |
html |
URL unfurling preferences |
ChatSettingsConnections |
html |
Integrations management |
ChatSettingsMaintenance |
html |
Admin tools (delete rooms, diagnostics) |
Sidebar Items
| Pattern |
Parent |
Purpose |
ChatSidebarNode |
html |
Base tree node (draggable, selectable) |
ChatRoomItem |
html |
Room in sidebar with unread badge |
ChatFolderItem |
html |
Folder with expand/collapse |
ChatOnlineAvatars |
html |
Online user avatar row |
ChatMiniProfile |
html |
User hover card |
Utility
| Pattern |
Parent |
Purpose |
ChatToast |
html |
Temporary notification |
ChatEmojiPicker |
html |
Floating emoji picker |
Call UI (chat_calls_client.js)
| Pattern/Object |
Parent |
Purpose |
ChatCallManager |
object |
WebRTC peer connections, media tracks, call state |
ChatCallIndicator |
html |
Room header badge: active call, join buttons |
ChatCallBar |
html |
Controls during call: mute, video, screen share, leave, grid |
ChatCallRoomBadge |
html |
Room sidebar badge showing participant count |
Key Design Patterns
Effective User Resolution
Socket handlers resolve user via: socket.user (authenticated) → socket.guestUser (guest) → null
Presence Tracking
onlineUsers Map: username → { sockets: Set<socket.id>, chat_name }
socketToUser Map: socket.id → username
userRooms Map: socket.id → Set<room_id>
Message Provenance
Every message tracks its edit/delete history:
provenance: [
{ action: 'created', user: 'alice', name: 'Alice', at: 1700000000 },
{ action: 'edited', user: 'alice', name: 'Alice', at: 1700000060 }
]
AI Key Encryption
User API keys are encrypted with AES-256-GCM. Stored as "iv:tag:encrypted" (hex, colon-separated). Masked display shows first 7 + last 4 characters.
Testing
- Start server and open
/chat
- Verify: lobby loads, room list populates, nickname prompt appears on first visit
- Verify messaging: send, edit, delete, typing indicator
- Verify DMs: create DM, send messages, check unread badges
- Verify calls: join call, audio/video toggle, screen share
- Verify socket flows: trace updates, browser panel sync in multiple clients
- Verify admin: delete empty rooms (requires
chat_admin group)