tacel-chat
v1.2.0
Published
Universal chat module for Tacel Electron apps -- conversation and room modes with presence, read receipts, file attachments, reply system, pinned messages, in-chat search, confirm dialogs, resizable sidebar, responsive design, context menus, @mentions, 30
Maintainers
Readme
Tacel Chat Module
A universal, app-agnostic chat module for Tacel Electron applications. Provides direct messaging, team/group conversations, real-time updates, presence indicators, read receipts, file attachments (drag-and-drop + file picker), @mentions, reply system, files panel, pinned messages panel, in-chat search, themed confirm dialogs, resizable sidebar, responsive design, context menus, configurable labels, and full CSS variable theming with 30 built-in themes -- all from a single shared codebase.
Table of Contents
- Overview
- File Structure
- Installation & Quick Start
- Modes
- Configuration & Callbacks
- Features
- File Attachments
- Reply System
- Files Panel
- Pinned Messages
- In-Chat Search
- Confirm Dialog
- Resizable Sidebar
- Responsive Design
- Context Menus
- Configurable Labels
- Universal Data Formats
- Frontend Components
- CSS Theming
- Built-in Themes (30)
- Backend API Factory
- Real-Time Layer
- Presence System
- Unread Counts & Read Receipts
- @Mentions & Notifications
- User Sync / Registration
- Public API
- IPC Channel Reference
- Database Schema
- Test App
- Integration Guide -- Conversation Mode
- Integration Guide -- Room Mode
- Security & Public Module Guidelines
1. Overview
Six Tacel Electron apps previously had their own separate chat implementations. This module replaces all of them with a single shared codebase supporting two modes:
- Conversation mode -- sidebar with direct messages + team conversations, presence, read receipts, seen indicators (used by Electron-template, Tech-Portal, ShipWorks, Office-HQ, Admin-Pro)
- Room mode -- tabbed chat rooms with polling, file attachments, @mentions, notifications (used by Wire-Scheduler)
Key Features
- Two modes -- conversation-based or room-based via
modeconfig - File attachments -- drag-and-drop, file picker, and clipboard paste (Ctrl+V screenshots), any file type/size, inline preview for images/PDFs, Open + Open Folder buttons
- Files panel -- collapsible sidebar listing all files in the current chat with click-to-scroll, open, right-click context menu
- Reply system -- right-click > Reply, accent-striped reply bar above input, clickable reply references on messages with scroll-to-original + highlight animation
- Pinned messages -- pin/unpin messages via context menu, pinned panel sidebar with badge count, click-to-scroll, pin icon on pinned messages in the chat
- In-chat search -- keyword search within current conversation/room, highlighted matching text, click result to scroll to message
- Confirm dialog -- themed in-app modal replacing
window.alert()/window.confirm(), inherits theme CSS variables - Resizable sidebar -- drag handle on sidebar edge, 200-500px range, configurable via
features.resizableSidebar - Responsive design -- adapts to small screens (700px, 520px, 380px breakpoints), stacked layout, overlay panels
- Silent refresh -- polling re-renders only when messages change, preserves scroll position
- Context-aware @mentions -- mention list filtered to participants of the active conversation/room
- Date separators -- "Today", "Yesterday", and full date labels between message groups
- Context menus -- right-click on messages, conversations, tabs, files, users with fully configurable items
- Configurable labels -- every UI text string is overridable for localization or customization
- @Mentions --
@trigger, filtered user popup, keyboard navigation, highlight rendering - Presence -- online/offline dots, heartbeat, "Last seen" text
- Read receipts -- per-message seen indicators with "checkmark Seen" tooltip
- Unread counts -- badge on conversations/tabs
- 30 built-in themes -- light, dark, and app-specific presets, all via CSS variables
- Backend API factory -- optional IPC handler registration using app-provided DB functions
- User sync utility -- cross-app user registration for shared APP-CHATS database
- Zero app-specific code -- module knows nothing about IPC, DB schemas, or network paths
2. File Structure
chat-module/
|-- index.js # Entry: exports { TacelChat, initChatAPI, syncUser, themes, ConfirmDialog }
|-- package.json # npm: tacel-chat
|-- README.md # This file
|-- chat.js # Main frontend: init, state, public API, send chain
|-- chat.css # All styles with CSS variables (~2500 lines)
|-- themes.js # 30 built-in theme presets
|-- chat-api.js # Backend IPC handler factory (optional)
|-- chat-sync.js # User sync for APP-CHATS registration
|-- components/
| |-- sidebar.js # Conversation list (conversation mode) + resizable drag handle
| |-- tabs.js # Tab bar (room mode) + actions area for panel toggles
| |-- message-area.js # Message display + input + reply bar + drop zone + panel orchestration
| |-- message.js # Single message rendering (bubble + card modes) + pin icon
| |-- attachment.js # File attachment system (drag-drop, picker, preview bar, card rendering)
| |-- files-panel.js # Files panel sidebar (lists all files in current chat)
| |-- pinned-panel.js # Pinned messages panel (lists pinned messages, click-to-scroll, unpin)
| |-- search-panel.js # In-chat search panel (keyword search, highlighted results, click-to-scroll)
| |-- confirm.js # Themed confirm/alert dialog (replaces window.alert/confirm)
| |-- context-menu.js # Right-click context menus (messages, conversations, tabs, files, users)
| |-- new-chat.js # New conversation modal
| |-- mention.js # @mention popup
| |-- presence.js # Presence helpers
|-- utils/
|-- format.js # Time formatting, date grouping, date dividers
|-- dom.js # DOM helpers, escape HTML, scroll utilities
|-- linkify.js # URL detection, mention highlighting3. Installation & Quick Start
npm install tacel-chatconst { TacelChat } = require('tacel-chat');
const chat = new TacelChat();
chat.initialize(document.getElementById('chat-container'), {
mode: 'conversation',
currentUsername: 'pierre',
theme: 'shipworks',
features: {
directMessages: true,
teamConversations: true,
presence: true,
readReceipts: true,
unreadCounts: true,
search: true,
newChat: true,
attachments: true,
attachmentPreview: true,
},
onFetchUsers: async () => { /* return users */ },
onFetchConversations: async (username) => { /* return conversations */ },
onFetchMessages: async (conversationId) => { /* return messages */ },
onSendMessage: async (conversationId, content, attachment, replyTo) => { /* send */ },
onUploadAttachment: async (attachment) => { /* upload, return { success, path, name, type, size } */ },
onOpenFile: async (filePath) => { /* open file */ },
onOpenFolder: async (filePath) => { /* show in folder */ },
// ... more callbacks
});<link rel="stylesheet" href="./node_modules/tacel-chat/chat.css">4. Modes
Conversation Mode (mode: 'conversation')
Sidebar + message area layout. Supports direct messages and team conversations. Users see a conversation list on the left, click to open, and chat on the right. Includes presence, read receipts, unread counts, search, and new chat modal. Sidebar is resizable via drag handle.
Room Mode (mode: 'room')
Tab bar + message area layout. Predefined rooms (e.g. General, Office, Dev) shown as tabs with search/pinned/files action buttons on the same row. Supports file attachments, @mentions, and polling-based refresh with silent updates. No sidebar, no direct messages, no read receipts.
5. Configuration & Callbacks
chat.initialize(container, {
mode: 'conversation', // 'conversation' or 'room'
currentUsername: 'pierre',
features: {
directMessages: true,
teamConversations: true,
teamManagement: false, // Admin-Pro only
presence: true,
readReceipts: true,
unreadCounts: true,
search: true,
newChat: true,
attachments: true, // Enable file attachments (drag-drop + picker)
attachmentPreview: true, // Enable inline image/PDF previews
mentions: false, // Enable @mentions (room mode)
mentionNotifications: false,
urlLinkify: true,
tabs: false, // Tab bar (room mode)
darkMode: false,
resizableSidebar: true, // Drag-to-resize sidebar (conversation mode)
pinnedMessages: true, // Pinned messages panel
searchMessages: true, // In-chat search panel
},
// Room mode config
rooms: [
{ type: 'general', name: 'General', icon: 'fas fa-comments' },
{ type: 'office', name: 'Office', icon: 'fas fa-building' },
],
defaultRoom: 'general',
// Timing
refreshIntervalMs: 5000,
heartbeatMs: 5000,
presenceStaleMs: 10000,
messagePollingMs: 0, // Room mode polling (Wire-Scheduler uses 2000)
// --- Data callbacks ---
onFetchUsers: async () => [],
onFetchConversations: async (currentUsername) => [],
onFetchMessages: async (conversationId) => [],
onSendMessage: async (conversationId, content, attachment, replyTo) => {},
onStartDirect: async (currentUsername, otherUserId) => {},
onFetchUnreadCounts: async (currentUsername) => [],
onMarkRead: async (conversationId, currentUsername) => {},
onFetchMessageReads: async (conversationId) => [],
// --- Presence callbacks ---
onPresenceHeartbeat: async (currentUsername) => {},
onPresenceOffline: async (currentUsername) => {},
onFetchDirectPeers: async (currentUsername) => [],
// --- Team management (optional) ---
onCreateTeam: async (currentUsername, name, memberIds) => {},
onUpdateTeam: async (conversationId, name, memberIds, currentUsername) => {},
onDeleteTeam: async (conversationId) => {},
onListTeams: async () => [],
// --- Attachment callbacks ---
onUploadAttachment: async (attachment) => {}, // { name, type, size, path, file }
onOpenFile: async (filePath) => {},
onOpenFolder: async (filePath) => {}, // Show file in folder (shell.showItemInFolder)
onGetFileContent: async (filePath) => {}, // Return base64 for image/PDF preview
// --- Mention callbacks ---
onFetchMentionUsers: async (conversationId) => [], // Context-aware: receives active conversation ID
onMarkRoomNotificationsRead: async (roomId, userId) => {},
// --- Pinned message callbacks ---
onPinMessage: async (msg, conversationId) => {}, // Pin or unpin a message
onUnpinMessage: async (msg, conversationId) => {}, // Unpin a message from the panel
onFetchPinnedMessages: async (conversationId) => [], // Return [{ id, content, senderName, timestamp }]
// --- Context menu action callbacks ---
onDeleteMessage: (msg) => {},
onQuoteMessage: (msg) => {},
onForwardMessage: (msg) => {},
onEditMessage: (msg) => {},
// --- Other callbacks ---
onLinkClick: (url) => {},
// --- Theme ---
theme: 'shipworks', // String preset name or object of CSS variables
// --- Labels (all overridable) ---
labels: { /* see Section 16 */ },
});6. Features
All features are opt-in via the features config object:
| Feature | Default | Description |
|---------|---------|-------------|
| directMessages | true | Show direct message conversations in sidebar |
| teamConversations | true | Show team conversations in sidebar |
| teamManagement | false | Enable create/edit/delete team (Admin-Pro) |
| presence | true | Online/offline dots and status text |
| readReceipts | true | "checkmark Seen" on last own message |
| unreadCounts | true | Unread badges on conversations |
| search | true | Search bar in sidebar |
| newChat | true | "+" button to start new conversation |
| attachments | false | File attachments (drag-drop + picker + preview) |
| attachmentPreview | false | Inline image/PDF previews on messages |
| mentions | false | @mention autocomplete popup |
| mentionNotifications | false | Backend mention notification processing |
| urlLinkify | true | Auto-detect and linkify URLs in messages |
| tabs | false | Tab bar for room mode |
| darkMode | false | Dark mode support |
| resizableSidebar | true | Drag-to-resize sidebar in conversation mode |
| pinnedMessages | true | Pinned messages panel with toggle button |
| searchMessages | true | In-chat search panel with toggle button |
7. File Attachments
Enabled via features.attachments: true. Supports any file type and any file size -- the implementing app handles storage (e.g. copy to shared drive).
Drag-and-Drop
Drag a file onto the message area > a dashed overlay appears with "Drop file to attach" > release to select the file.
Clipboard Paste
Press Ctrl+V (or Cmd+V) while the input textarea is focused to paste an image from the clipboard. This supports screenshots taken with Win+Shift+S (Windows Snipping Tool), Print Screen, or any other tool that copies an image to the clipboard. The pasted image is automatically named screenshot-YYYY-MM-DDTHH-MM-SS.png and appears in the attachment preview bar, ready to send.
When a clipboard image is pasted, the module reads it as base64 and includes the data field in the attachment object passed to onUploadAttachment. The app should check for data (base64) when sourcePath is not available.
File Picker
Click the paperclip button in the input area > standard file picker opens > select any file.
Attachment Preview Bar
When a file is selected (via drag-drop, picker, or clipboard paste), a preview bar appears above the input showing:
- File type icon (color-coded by type: image, PDF, Word, Excel, archive, code, audio, video, etc.)
- File name (truncated with ellipsis)
- File size (formatted: KB, MB, GB)
- x button to remove the attachment
File-Only Messages
Users can send a file without any text. When a message has an attachment but no text content, the message bubble/text element is omitted entirely -- only the sender header and attachment card are shown. No empty bubble is rendered.
Attachment Card (on messages)
When a message has an attachment, it renders as a card with:
- Inline preview for images (max 200px height) and PDFs (160px embed) when
attachmentPreview: true - File info row with type icon, file name, file size
- Open button (opens the file in the default app)
- Open Folder button (shows the file in its folder via
shell.showItemInFolder)
Upload Flow
- User selects file (drag-drop or picker)
- Preview bar appears above input
- User types message (optional) and clicks Send
- Module calls
onUploadAttachment({ name, type, size, path, file })> app copies file to storage, returns{ success, path, name, type, size } - Module calls
onSendMessage(conversationId, content, uploadedAttachment, replyTo)> app stores message with attachment data - Messages reload, attachment card renders on the message
File Icon Mapping
The module maps file extensions to FontAwesome icons:
- Images -- jpg, png, gif, bmp, webp, svg >
fa-file-image - PDF >
fa-file-pdf - Word -- doc, docx, odt, rtf >
fa-file-word - Excel -- xls, xlsx, csv, ods >
fa-file-excel - PowerPoint -- ppt, pptx >
fa-file-powerpoint - Archives -- zip, rar, 7z, tar, gz >
fa-file-archive - Code -- js, py, html, css, json, sql, etc. >
fa-file-code - Audio -- mp3, wav, ogg, flac >
fa-file-audio - Video -- mp4, avi, mkv, mov >
fa-file-video - Text -- txt, log, md, ini >
fa-file-alt - Other >
fa-file
8. Reply System
Right-click any message > Reply > a reply bar appears above the input area.
Reply Bar (above input)
- 4px accent stripe on the left edge
- Header row: reply icon + "Replying to" label + sender name in accent color
- Message preview: first 140 characters of the original message
- x close button on the right (turns red on hover)
- Animated slide-in
Reply Reference (on messages)
When a message is a reply, it shows a reference card above the message:
- 3px accent left border with tinted background
- Sender name with reply icon
- Message preview (first 120 characters)
- Clickable -- clicking scrolls to and highlights the original message
Scroll-to-Original
When you click a reply reference:
- The messages area smoothly scrolls to center the original message
- The original message gets a 1.5s flash highlight animation (accent color fade)
- Each message has a
data-msg-idattribute for targeting
Reply Data Flow
setReply(msg) > reply bar shown > user sends > _send() includes replyTo: { id, senderName, content } > onSendMessage(conversationId, content, attachment, replyTo) > app stores reply data > messages reload with replyTo on the message object.
9. Files Panel
A collapsible panel on the right side of the message area that lists all file attachments in the current conversation/room.
Toggle
- Folder icon button in the chat header (conversation mode) or tab bar (room mode)
- Badge showing the count of files in the current chat
- Click to open/close the panel
- Active state: accent border + tinted background
Panel Contents
- Header: "Files" title with folder icon + close button
- File list (newest first): each item shows:
- File type icon (color-coded)
- File name (truncated)
- Meta line: file size . sender name . time
- Open and Open Folder buttons (appear on hover)
- Empty state: folder icon + "No files shared yet"
Interactions
- Click a file item > closes the panel and scrolls to the message containing that file (with highlight animation)
- Right-click a file item > context menu with: Open File, Open Folder, Scroll to Message
- Open/Open Folder buttons on hover > direct file actions
10. Pinned Messages
Pin important messages to make them easily accessible. Controlled by the app via callbacks.
Pinning a Message
- Right-click a message > Pin Message (or Unpin Message if already pinned)
- The action routes through
chat.pinMessage(msg)which calls the app'sonPinMessagecallback with the message and conversation ID - After pinning, the pinned panel reloads and messages re-render to show pin icons
Pin Icon on Messages
Pinned messages display a small thumbtack icon in the message header (between sender name and timestamp), styled in the accent color.
Pinned Panel
- Thumbtack icon button in the chat header (conversation mode) or tab bar (room mode)
- Badge showing the count of pinned messages
- Click to open/close the panel
Panel Contents
- Header: "Pinned Messages" title with pin icon + close button
- Pinned message list: each item shows:
- Sender name
- Message preview (2-line clamp)
- Timestamp
- x unpin button on hover
- Empty state: pin icon + "No pinned messages"
Interactions
- Click a pinned item > closes the panel and scrolls to the original message (with highlight animation)
- x button on hover > unpins the message via
onUnpinMessagecallback
Callbacks
onPinMessage: async (msg, conversationId) => {
// msg.isPinned tells you current state -- toggle accordingly
// App handles the actual pin/unpin in its database
},
onUnpinMessage: async (msg, conversationId) => {
// Called when user clicks x on a pinned panel item
},
onFetchPinnedMessages: async (conversationId) => {
// Return array of { id, content, senderName, timestamp }
return [];
},11. In-Chat Search
Search within the current conversation or room for keywords.
Toggle
- Search icon button in the chat header (conversation mode) or tab bar (room mode)
- Click to open/close the search panel
Panel Contents
- Header: "Search Messages" title with search icon + close button
- Search input: auto-focuses when panel opens, real-time filtering as you type
- Result count: "X results" displayed below the input
- Result list: each item shows:
- Sender name
- Message text with highlighted matching keywords (
<mark>tags) - Timestamp
- Empty state: search icon + "Search for messages"
- No results state: "No messages found"
Interactions
- Type in the search input > results filter in real-time
- Click a result > closes the panel and scrolls to the original message (with highlight animation)
Data Flow
The search panel receives all messages from renderMessages() and filters them client-side by keyword match on msg.content. No backend callback is needed -- search is entirely frontend.
12. Confirm Dialog
A themed in-app modal that replaces native window.alert() and window.confirm() popups. Exported from the module so apps can use it for their own actions too.
Usage
const { ConfirmDialog } = require('tacel-chat');
const dialog = new ConfirmDialog(mountElement); // Mount inside .tacel-chat for theme inheritance
// Alert (single OK button)
await dialog.alert('Message deleted successfully', {
title: 'Deleted',
icon: 'fas fa-trash',
buttonText: 'OK'
});
// Confirm (OK + Cancel)
const confirmed = await dialog.confirm('Are you sure you want to pin this message?', {
title: 'Pin Message',
icon: 'fas fa-thumbtack',
confirmText: 'Pin',
cancelText: 'Cancel'
});
if (confirmed) { /* user clicked Pin */ }Features
- Dark semi-transparent backdrop with blur effect
- Pop-in animation (scale + fade)
- Icon (FontAwesome) + title + message
- Primary/secondary buttons with accent color styling
- Click outside to dismiss (resolves as cancel)
- Inherits theme -- mounts inside the
.tacel-chatcontainer so all CSS variables cascade - Exported from
index.jsso apps can create their own instances
13. Resizable Sidebar
The conversation list sidebar can be resized by dragging its right edge.
Behavior
- A thin 6px drag handle on the right edge of the sidebar
- Highlights with accent color on hover and during drag
- Min width: 200px
- Max width: 500px
- Cursor changes to
col-resizeduring drag - Grid layout updates in real-time as you drag
Configuration
features: {
resizableSidebar: true, // Default: true. Set to false to disable.
}Cleanup
Event listeners are properly cleaned up on destroy().
14. Responsive Design
The chat module adapts to small container/window sizes via CSS media queries.
Breakpoints
| Breakpoint | Changes | |------------|---------| | <= 700px | Sidebar narrows to 220px, side panels shrink to 200px, modals cap at 90vw | | <= 520px | Sidebar stacks vertically (full width, max 40vh), resize handle hidden, side panels overlay as absolute drawers with shadow, tabs/topbar compact | | <= 380px | Ultra-compact: smaller header padding, 28px action buttons, tighter gaps |
Panel Behavior at Small Sizes
At 520px and below, the files panel, pinned panel, and search panel become absolute-positioned overlays with a shadow, rather than taking up inline space. This ensures the message area remains usable.
Confirm Dialog
The confirm dialog scales down at each breakpoint with smaller padding, font sizes, and button sizes.
15. Context Menus
Right-click context menus are available on messages, conversations, tabs, files, and users. All menu item labels are configurable via labels.
Message Context Menu
- Copy Text -- copies message content
- Reply -- sets the reply bar (built-in, not a callback)
- Quote -- inserts quoted text into input
- Forward, Edit Message, Delete Message -- via callbacks
- Pin/Unpin Message -- routes through
chat.pinMessage()for proper panel reload
Conversation Context Menu
- Mark as Read, Mute/Unmute Notifications, Pin/Unpin to Top
- Leave Conversation, Delete Conversation
File Context Menu (in Files Panel)
- Open File -- opens in default app
- Open Folder -- shows in file explorer
- Scroll to Message -- scrolls to and highlights the message
Tab Context Menu (room mode)
- Open Room, Refresh Messages, Search Messages, Clear Chat History
16. Configurable Labels
Every UI text string is overridable via the labels config object. This enables localization or custom wording.
labels: {
sidebarTitle: 'Messages',
searchPlaceholder: 'Search conversations...',
newChatTitle: 'New Direct Message',
newChatSearch: 'Search users...',
teamsHeader: 'Teams',
directsHeader: 'Direct Messages',
noConversations: 'No conversations yet',
noResults: 'No conversations found',
noMessages: 'No messages yet',
noMessagesHint: 'Send a message to start the conversation',
inputPlaceholder: 'Type a message...',
roomTitle: 'Chat Center',
loadingText: 'Loading...',
errorText: 'Failed to load chat',
retryText: 'Retry',
seenBy: 'Seen by',
online: 'Online',
offline: 'Offline',
lastSeen: 'Last seen',
typing: 'typing...',
noUsers: 'No users available',
// Files panel
filesPanel: 'Files',
noFiles: 'No files shared yet',
openFile: 'Open File',
openFolder: 'Open Folder',
scrollToMessage: 'Scroll to Message',
// Pinned panel
pinnedPanel: 'Pinned Messages',
noPinned: 'No pinned messages',
// Search panel
searchPanel: 'Search Messages',
searchPlaceholderMessages: 'Search messages...',
noSearchResults: 'No messages found',
// Context menu labels
copyText: 'Copy Text',
reply: 'Reply',
quote: 'Quote',
forward: 'Forward',
editMessage: 'Edit Message',
deleteMessage: 'Delete Message',
pinMessage: 'Pin Message',
unpinMessage: 'Unpin Message',
markAsRead: 'Mark as Read',
muteNotifications: 'Mute Notifications',
unmuteNotifications: 'Unmute',
pinToTop: 'Pin to Top',
unpin: 'Unpin',
leaveConversation: 'Leave Conversation',
deleteConversation: 'Delete Conversation',
refreshMessages: 'Refresh Messages',
searchMessages: 'Search Messages',
clearChat: 'Clear Chat History',
openRoom: 'Open Room',
sendDirectMessage: 'Send Direct Message',
viewProfile: 'View Profile',
}17. Universal Data Formats
Message
{
id: number|string,
conversationId: number|string,
senderId: number|string,
senderName: string,
content: string,
timestamp: Date|string,
isOwn: boolean,
isPinned: boolean, // Set automatically by the module based on pinned panel data
hasAttachment: boolean,
attachmentName: string|null,
attachmentPath: string|null,
attachmentType: string|null,
attachmentSize: number|null,
seenBy: Array<{ userId, username, readAt }>|null,
replyTo: { id, senderName, content }|null,
meta: object
}Conversation
{
id: number|string,
name: string,
type: 'direct'|'team'|'room',
lastMessage: string|null,
lastMessageTime: Date|string|null,
unreadCount: number,
participants: Array<string>,
meta: object
}User
{
id: number|string,
username: string,
isOnline: boolean,
lastSeen: Date|string|null,
app: string|null,
meta: object
}18. Frontend Components
Sidebar (sidebar.js) -- Conversation mode
Conversation list split into "Teams" and "Direct Messages". Header with search + new chat button. Items: avatar, name + presence dot, preview, timestamp, unread badge, active highlight. Resizable via drag handle on right edge (configurable).
Tabs (tabs.js) -- Room mode
Top bar with title, tab items with icon + label + optional badge, active state with accent border. Actions area on the right side of the tabs row for search/pinned/files toggle buttons.
Message Area (message-area.js)
Header (avatar + name + status + action buttons), body wrapper (messages + files/pinned/search panels), reply bar, attachment preview bar, input area (paperclip + textarea + send button). Handles drag-and-drop, reply state, scroll-to-message, panel orchestration, and file context menu dispatch. The input textarea auto-resizes as the user types multi-line content. Silent refresh: skips re-render when messages haven't changed, preserves scroll position.
Message (message.js)
- Conversation mode: Avatar (rounded square), sender + pin icon + time header, bubble (accent for own, neutral for others), attachment card, seen indicator
- Room mode: Avatar (circle) always left, sender + pin icon + time, card-style content, attachment card, highlighted mentions + URLs
- Both modes: reply reference above message (clickable, scrolls to original), pin icon on pinned messages
Attachment (attachment.js)
Complete file attachment system:
createControls()-- paperclip button + hidden file inputcreatePreviewBar()-- preview bar above input (icon + name + size + x remove)createDropZone(messagesEl)-- drag-and-drop overlay on messages areaconsumeAttachment()-- get selected file and clearrenderAttachmentPreview(msg, container)-- render attachment card on a message- Exports:
getFileIcon(filename),formatFileSize(bytes)
Files Panel (files-panel.js)
Collapsible right-side panel listing all files in the current chat. Toggle button with badge in header. Items: icon + name + meta + action buttons. Click to scroll, right-click for context menu.
Pinned Panel (pinned-panel.js)
Collapsible right-side panel listing all pinned messages. Toggle button with badge count in header/tab bar. Items: sender + preview + timestamp + unpin button. Click to scroll to original message.
Search Panel (search-panel.js)
Collapsible right-side panel with search input. Real-time keyword filtering, highlighted matching text, result count. Click result to scroll to original message.
Confirm Dialog (confirm.js)
Themed modal overlay with icon, title, message, and action buttons. Replaces window.alert() and window.confirm(). Mounts inside .tacel-chat container for CSS variable inheritance. Exported from index.js.
Context Menu (context-menu.js)
Positioned context menu with icon + label items. Supports: separators, headers, disabled items, danger items, hidden items. Target types: message, conversation, tab, background, user, file.
New Chat Modal (new-chat.js)
Overlay, search input, user list (excludes self + existing direct peers), click > create conversation.
@Mention Popup (mention.js)
Triggered by @, filtered user list (context-aware -- filtered to conversation/room participants), keyboard navigation (arrows/tab/enter/escape), inserts @username .
Presence (presence.js)
Helpers: checkOnline(user, staleMs), getPresenceText(user, staleMs) -- returns "Online" or "Last seen X ago".
19. CSS Theming
Theme System
Themes can be applied three ways:
// 1. Built-in preset by name
chat.initialize(container, { theme: 'dark' });
// 2. Custom CSS variables object
chat.initialize(container, { theme: { '--chat-bg': '#111', '--chat-accent': '#ff6600' } });
// 3. Extend a preset
const { themes } = require('tacel-chat');
chat.initialize(container, { theme: { ...themes.dark, '--chat-accent': '#ff6600' } });CSS Variables Reference
| Variable | Description |
|----------|-------------|
| --chat-bg | Main background |
| --chat-sidebar-bg | Sidebar background |
| --chat-border | Border color |
| --chat-text | Primary text |
| --chat-text-secondary | Secondary text |
| --chat-text-muted | Muted text |
| --chat-accent | Accent color (buttons, links, badges, reply bars, pin icons) |
| --chat-accent-transparent | Accent with alpha (highlights, tints) |
| --chat-accent-gradient | Gradient for send button |
| --chat-hover | Hover background |
| --chat-own-bubble-bg/border/text | Own message bubble |
| --chat-other-bubble-bg/border/text | Other message bubble |
| --chat-avatar-bg/text | Avatar styling |
| --chat-online-color/glow | Online presence dot |
| --chat-offline-color | Offline presence dot |
| --chat-unread-bg/text | Unread badge |
| --chat-input-bg/border/focus-border | Input field |
| --chat-send-bg/text | Send button |
| --chat-seen-color | Read receipt checkmarks |
| --chat-mention-bg/text | @mention highlight |
| --chat-attachment-bg | Attachment card background |
| --chat-attachment-btn-bg/hover | Paperclip button |
| --chat-attachment-active-bg/text | Active attachment indicator |
| --chat-link-color | URL link color |
| --chat-tab-bg/active-bg/active-color/active-border | Tab bar |
| --chat-modal-backdrop/bg/shadow | Modal overlay (used by confirm dialog + new chat modal) |
| --chat-error-color | Error text |
| --chat-transition-speed | Animation speed |
| --chat-radius / --chat-radius-sm | Border radius |
Apps can reference their own variables: '--chat-accent': 'var(--primary)'.
20. Built-in Themes (30)
All themes exported from themes.js via require('tacel-chat').themes.
Light Themes (18)
| Name | Accent | Background |
|------|--------|------------|
| default | #1976d2 | #ffffff |
| light | #1976d2 | #ffffff |
| ocean | #00897b | #f0fafa |
| forest | #2e7d32 | #f2f7f2 |
| sunset | #e65100 | #fff8f0 |
| rose | #c2185b | #fff5f8 |
| lavender | #673ab7 | #f8f5ff |
| slate | #495057 | #f8f9fa |
| solarized-light | #268bd2 | #fdf6e3 |
| catppuccin-latte | #8839ef | #eff1f5 |
| github-light | #0969da | #ffffff |
| office-hq | #c9a227 | #ffffff |
| shipworks | #1976d2 | #ffffff |
| coral | #ff5733 | #fff5f3 |
| mint | #00c853 | #f0fff4 |
| amber | #ffa000 | #fffbf0 |
| indigo | #303f9f | #f5f5ff |
| cream | #8b7750 | #fefcf3 |
Dark Themes (12)
| Name | Accent | Background |
|------|--------|------------|
| dark | #89b4fa | #1e1e2e |
| midnight | #6c7bd4 | #0f0f1a |
| nord | #88c0d0 | #2e3440 |
| dracula | #bd93f9 | #282a36 |
| monokai | #a6e22e | #272822 |
| solarized-dark | #268bd2 | #002b36 |
| catppuccin-mocha | #cba6f7 | #1e1e2e |
| github-dark | #58a6ff | #0d1117 |
| tech-portal | #53c1de | #1a1a2e |
| abyss | #4fc3f7 | #060818 |
| neon | #00ff88 | #0a0a0a |
| cherry | #dc3c50 | #1a0a0e |
21. Backend API Factory
Optional factory that registers IPC handlers using app-provided DB functions:
const { initChatAPI } = require('tacel-chat/chat-api');
initChatAPI(ipcMain, {
channelPrefix: '',
dbQuery: (sql, params) => db.query('APP-CHATS', sql, params),
dbGetOne: (sql, params) => db.getOne('APP-CHATS', sql, params),
dbInsert: (table, data) => db.insert('APP-CHATS', table, data),
dbUpdate: (sql, cond, params) => db.update('APP-CHATS', sql, cond, params),
socketEmit: (event, payload) => socketClient.emit(event, payload),
broadcastRenderer: (eventName, payload) => { /* BrowserWindow broadcast */ },
});22. Real-Time Layer
Module does not own the transport. Apps connect their Socket.IO/polling to the instance's event methods:
chat.onNewMessage({ conversationId, messageId, sender, senderId, content, timestamp })
chat.onReadEvent({ conversationId, username, userId })
chat.onNewConversation({ conversationId })
chat.onPresenceUpdate({ userId, username, isOnline, lastSeen })For room mode, messagePollingMs enables internal polling (Wire-Scheduler uses 2000ms). The module uses silent refresh -- it skips DOM re-rendering when messages haven't changed and preserves scroll position when they have.
23. Presence System
- On init:
onPresenceHeartbeat()immediately - Every
heartbeatMs:onPresenceHeartbeat() - On
beforeunload:onPresenceOffline() - Rendering: check
lastSeenagainstpresenceStaleMs
Apps without presence set features.presence: false.
24. Unread Counts & Read Receipts
Unread counts: derived from messages with no read receipt from current user. Fetched periodically and on socket events. Displayed as badges on conversations.
Read receipts: opening a conversation marks all unread as read. Only the last own message shows "checkmark Seen" with tooltip of who saw it.
Room mode does not use read receipts.
25. @Mentions & Notifications
Enabled via features.mentions: true.
Frontend: @ triggers popup, filtered users (context-aware -- filtered to participants of the active conversation/room), keyboard nav, inserts @username . Rendering: escape HTML > linkify URLs > highlight mentions (.tc-mention / .tc-mention-self).
Backend (features.mentionNotifications: true): Extract /@(\w+)/g, resolve to user IDs, create notification rows, skip self-mentions. Room open > mark read.
26. User Sync / Registration
For conversation mode, users must be in APP-CHATS:
const { syncUser } = require('tacel-chat/chat-sync');
await syncUser({ dbQuery, dbGetOne, dbInsert, dbUpdate, original_id, app, username });Resolution: global_key first (cross-app unification) > (original_id, app) fallback > create if not found.
Room mode uses app's own users table -- no sync needed.
27. Public API
| Method | Description |
|--------|-------------|
| new TacelChat() | Create instance |
| instance.initialize(container, config) | Mount chat into DOM element |
| instance.refresh() | Re-fetch conversations/messages and re-render |
| instance.openConversation(id) | Programmatically open a conversation |
| instance.switchRoom(roomType) | Switch to a room tab (room mode) |
| instance.onNewMessage(payload) | Push real-time new message event |
| instance.onReadEvent(payload) | Push real-time read event |
| instance.onNewConversation(payload) | Push real-time new conversation event |
| instance.onPresenceUpdate(payload) | Push real-time presence event |
| instance.pinMessage(msg) | Programmatically pin/unpin a message |
| instance.destroy() | Clean up listeners, intervals, DOM |
Exported from index.js
const { TacelChat, initChatAPI, syncUser, normalizeUsername, themes, ConfirmDialog } = require('tacel-chat');28. IPC Channel Reference
Conversation Mode
| Channel | Params | Returns |
|---------|--------|---------|
| chat-list-users | -- | [{ id, username, is_online, last_seen }] |
| chat-presence-heartbeat | { current_username } | { success } |
| chat-presence-offline | { current_username } | { success } |
| chat-get-conversations | { current_username } | [{ id, type, display_name, last_message, last_timestamp }] |
| chat-get-messages | { conversation_id } | [{ id, content, timestamp, sender, sender_id, reply_to, attachment }] |
| chat-start-direct | { current_username, other_user_id } | { success, conversation_id } |
| chat-send-message | { conversation_id, current_username, content, attachment, reply_to } | { success, message_id } |
| chat-get-unread-counts | { current_username } | [{ conversation_id, unread_count }] |
| chat-mark-read | { conversation_id, current_username } | { success } |
| chat-get-message-reads | { conversation_id } | [{ message_id, user_id, username, read_at }] |
Attachment Channels
| Channel | Params | Returns |
|---------|--------|---------|
| chat-upload-attachment | { name, type, size, sourcePath } | { success, path, name, type, size } |
| chat-open-file | { path } | { success } |
| chat-open-folder | { path } | { success } |
| chat-get-file-content | { path } | { success, data: { content } } |
Pinned Message Channels
| Channel | Params | Returns |
|---------|--------|---------|
| chat-pin-message | { conversation_id, message_id } | { success } |
| chat-unpin-message | { conversation_id, message_id } | { success } |
| chat-get-pinned-messages | { conversation_id } | { success, data: [{ id, content, senderName, timestamp }] } |
29. Database Schema
Conversation Mode (APP-CHATS database)
CREATE TABLE chat_users (
id INT AUTO_INCREMENT PRIMARY KEY,
original_id INT,
app VARCHAR(50),
username VARCHAR(100),
global_key VARCHAR(100),
is_online TINYINT DEFAULT 0,
last_seen DATETIME
);
CREATE TABLE chat_conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200),
type ENUM('direct', 'team'),
created_by INT,
created_at DATETIME,
updated_at DATETIME
);
CREATE TABLE chat_participants (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT,
user_id INT,
status ENUM('active', 'removed') DEFAULT 'active'
);
CREATE TABLE chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT,
sender_id INT,
content TEXT,
timestamp DATETIME,
reply_to JSON,
attachment JSON
);
CREATE TABLE chat_message_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
message_id INT,
user_id INT,
read_at DATETIME
);30. Test App
A full test application is available at Random (rma,ticketing,more)/chat-test-app/:
- Electron app with split-screen: left side (conversation mode) + right side (room mode)
- Two users (Alice + Bob) with independent chat instances
- Theme selector per side with all 30 themes
- Mock database with seeded users, conversations, messages, and pinned message state
- Full attachment support: file upload to local
chat-attachments/folder, open file, open folder, inline image/PDF preview - Reply support: reply data stored and rendered
- Pin support: pin/unpin messages, pinned panel with badge, click-to-scroll
- Search support: in-chat keyword search with highlighted results
- Confirm dialog: all alert/confirm popups use themed in-app modal
- All features enabled: presence, read receipts, unread counts, search, new chat, attachments, mentions, pinned messages, search panel
Running the test app:
cd "Random (rma,ticketing,more)/chat-test-app"
npm install
npx electron .31. Integration Guide -- Conversation Mode
This section describes how the conversation-mode apps integrate the chat module with a shared database, cross-app user identity, and real-time Socket.IO relay. The module itself contains no database code, no SQL, no network paths, and no credentials -- all of that lives in each app's own backend.
Architecture Overview
+----------------+ +----------------+ +----------------+
| App A | | App B | | App C |
| (renderer) | | (renderer) | | (renderer) |
| tacel-chat | | tacel-chat | | tacel-chat |
+-------+--------+ +-------+--------+ +-------+--------+
| IPC | IPC | IPC
+-------+--------+ +-------+--------+ +-------+--------+
| App A | | App B | | App C |
| (main) | | (main) | | (main) |
| chat-api.js | | chat-api.js | | chat-api.js |
| chat.js | | chat.js | | chat.js |
+-------+--------+ +-------+--------+ +-------+--------+
| | |
v v v
+-------------------------------------------------+
| Shared MySQL Database |
| (e.g. APP-CHATS) |
| chat_users . chat_conversations . chat_messages |
| chat_participants . chat_message_reads |
+-------------------------------------------------+
| | |
v v v
+-------------------------------------------------+
| Socket.IO Relay Server |
| One app instance hosts (port 3001) |
| All others connect as clients |
| Events: new_message, read, new_conversation |
+-------------------------------------------------+Cross-App User Identity
Multiple apps share a single chat database. A user like "Pierre" may log into App A, App B, and App C -- but should see the same conversations and messages everywhere.
How it works:
- Each app has its own
userstable with its own user IDs - The shared chat database has a
chat_userstable with aglobal_keycolumn global_key=username.trim().toLowerCase()-- this is the cross-app identity key- When a user logs in, the app calls
syncUser({ original_id, app, username }) - Sync resolution order:
- First: find by
global_key(matches across all apps) - Fallback: find by
(original_id, app)pair (legacy match within one app) - Not found: create a new
chat_usersrow
- First: find by
Example: Pierre logs into Office-HQ (user ID 5) and ShipWorks (user ID 12). Both apps call syncUser with username: 'Pierre'. The global_key is 'pierre'. Both resolve to the same chat_users row, so Pierre sees the same conversations in both apps.
// In your app's login flow (main process):
const { syncUser } = require('tacel-chat/chat-sync');
// After successful login, sync the user into the shared chat database
const chatUser = await syncUser({
original_id: appUser.id, // The user's ID in THIS app's database
app: 'YourAppName', // Identifier for this app
username: appUser.username, // The display username (used for global_key)
// Provide your app's DB helpers:
dbQuery, dbGetOne, dbInsert, dbUpdate
});
// chatUser = { id, original_id, app, username, global_key, is_online, last_seen }Database Schema (Shared Chat Database)
The shared database (e.g. APP-CHATS) uses these tables:
-- Users from all apps, unified by global_key
CREATE TABLE chat_users (
id INT AUTO_INCREMENT PRIMARY KEY,
original_id INT, -- User's ID in their source app
app VARCHAR(50), -- Source app name (e.g. 'Office-HQ', 'ShipWorks')
username VARCHAR(100), -- Display username
global_key VARCHAR(100), -- Normalized: username.trim().toLowerCase()
is_online TINYINT DEFAULT 0,
last_seen DATETIME,
last_active DATETIME
);
-- Conversations (direct or team)
CREATE TABLE chat_conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200), -- NULL for direct, team name for teams
type ENUM('direct', 'team'),
created_by INT, -- chat_users.id
created_at DATETIME,
updated_at DATETIME
);
-- Who is in each conversation
CREATE TABLE chat_participants (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT,
user_id INT, -- chat_users.id
status ENUM('active', 'removed') DEFAULT 'active',
joined_at DATETIME,
removed_at DATETIME,
removed_by INT
);
-- Messages
CREATE TABLE chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT,
sender_id INT, -- chat_users.id
content TEXT,
timestamp DATETIME,
reply_to JSON, -- { id, senderName, content } or NULL
attachment JSON -- { name, type, size, path } or NULL
);
-- Read receipts (per-message, per-user)
CREATE TABLE chat_message_reads (
id INT AUTO_INCREMENT PRIMARY KEY,
message_id INT,
user_id INT, -- chat_users.id
read_at DATETIME
);Backend API Pattern
Each app registers IPC handlers in its chat-api.js. The pattern is identical across all conversation-mode apps -- only the DB connection import differs:
const db = require('../db/dynamic-connection'); // Your app's DB helper
const { emit: emitSocket, getSocket } = require('../main/socket-client');
const { BrowserWindow } = require('electron');
const CHAT_DB = 'APP-CHATS'; // Your shared chat database name
function initChatAPI(ipcMain) {
// Fallback: broadcast to all renderer windows when socket is down
function broadcastRenderer(eventName, payload) {
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send(`socket:event:${eventName}`, payload);
}
}
ipcMain.handle('chat-list-users', async () => {
return await db.query(CHAT_DB,
'SELECT id, username, app, is_online, last_seen, global_key FROM chat_users ORDER BY username ASC'
);
});
ipcMain.handle('chat-send-message', async (event, { conversation_id, current_username, content, attachment, reply_to }) => {
const gk = current_username.trim().toLowerCase();
const me = await db.getOne(CHAT_DB, 'SELECT id, username FROM chat_users WHERE global_key = ? LIMIT 1', [gk]);
// ... insert message, touch conversation, insert sender read receipt ...
// Emit via Socket.IO for real-time delivery to all connected apps
emitSocket('chat:new_message', { conversation_id, message_id, sender: me.username, content, timestamp });
// Fallback if socket is disconnected
const s = getSocket();
if (!s || !s.connected) {
broadcastRenderer('chat:new_message', { conversation_id, message_id, sender: me.username, content, timestamp });
}
return { success: true, message_id };
});
// ... other handlers: chat-get-conversations, chat-get-messages, chat-start-direct,
// chat-presence-heartbeat, chat-presence-offline, chat-get-unread-counts,
// chat-mark-read, chat-get-message-reads, chat-get-direct-peers
}Socket.IO Real-Time Layer
The module does not include Socket.IO -- apps manage their own socket infrastructure.
Socket Server (one app instance hosts):
- HTTP server + Socket.IO
Serveron a configurable port (default 3001) - Listens on
0.0.0.0so all machines on the network can connect - Relays three events to all connected clients:
chat:new_message-- new message sentchat:read-- conversation marked as readchat:new_conversation-- new conversation created
Socket Client (all app instances connect):
- Reads
socketServerUrlfromversion.json(e.g.http://192.168.x.x:3001) - Socket.IO client with
autoConnect: true - Exports
getSocket()andemit(event, payload)with auto-reconnect queuing
Configuration (version.json):
{
"socketServerUrl": "http://192.168.x.x:3001",
"isSocketHost": false
}One machine sets isSocketHost: true and starts the socket server. All others connect as clients.
Wiring Socket Events to the Module
In the renderer, listen for socket events and push them into the chat instance:
// Listen for socket events forwarded from main process
window.electronAPI.onSocketEvent('chat:new_message', (payload) => {
chat.onNewMessage(payload);
});
window.electronAPI.onSocketEvent('chat:read', (payload) => {
chat.onReadEvent(payload);
});
window.electronAPI.onSocketEvent('chat:new_conversation', (payload) => {
chat.onNewConversation(payload);
});Presence System
- On login:
syncUser()setsis_online = 1 - Every 5s:
onPresenceHeartbeat()>UPDATE chat_users SET is_online = 1, last_seen = NOW() WHERE global_key = ? - On
beforeunload:onPresenceOffline()>UPDATE chat_users SET is_online = 0, last_seen = NOW() WHERE global_key = ? - Module checks
last_seenagainstpresenceStaleMsto determine online/offline status
Unread Counts & Read Receipts
- Unread counts are derived from messages with no read receipt from the current user:
SELECT COUNT(*) FROM chat_messages m WHERE m.conversation_id = ? AND m.sender_id <> ? AND NOT EXISTS (SELECT 1 FROM chat_message_reads r WHERE r.message_id = m.id AND r.user_id = ?) - Mark read: insert
chat_message_readsrows for all unread messages in the conversation - Seen indicator: only the last own message shows "checkmark Seen" with tooltip of who saw it
32. Integration Guide -- Room Mode
This section describes how room-mode apps (e.g. Wire-Scheduler) integrate the chat module with their own app database, predefined rooms, file attachments on a network share, and polling-based refresh.
Architecture Overview
Room mode uses the app's own database (not the shared APP-CHATS database). Each app defines its own rooms, users, and messages tables.
+--------------------+
| Wire-Scheduler |
| (renderer) |
| tacel-chat |
| mode: 'room' |
+---------+----------+
| IPC
+---------+----------+
| Wire-Scheduler |
| (main process) |
| chat-api.js |
| chat.js (DB) |
+---------+----------+
|
+----+-----+
| App DB | +--------------------+
| (MySQL) | | Network Share |
| rooms | | (attachments) |
| messages | +--------------------+
| users |
+----------+Database Schema (App's Own Database)
-- Predefined chat rooms
CREATE TABLE chat_rooms (
id INT AUTO_INCREMENT PRIMARY KEY,
room_type VARCHAR(50), -- 'general', 'office', 'dev'
name VARCHAR(100),
description TEXT
);
-- Messages in rooms
CREATE TABLE chat_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
room_id INT, -- chat_rooms.id
user_id INT, -- users.id (app's own users table)
message TEXT,
created_at DATETIME,
has_attachment TINYINT DEFAULT 0,
attachment_path VARCHAR(500),
attachment_type VARCHAR(100),
attachment_name VARCHAR(255)
);
-- App's own users table (not chat-specific)
-- The app joins chat_messages.user_id to users.id for sender infoRoom Mode Configuration
chat.initialize(container, {
mode: 'room',
currentUsername: 'pierre',
rooms: [
{ type: 'general', name: 'General', icon: 'fas fa-comments' },
{ type: 'office', name: 'Office', icon: 'fas fa-building' },
{ type: 'dev', name: 'Dev', icon: 'fas fa-code' },
],
defaultRoom: 'general',
messagePollingMs: 2000, // Poll for new messages every 2 seconds
features: {
attachments: true,
attachmentPreview: true,
mentions: true,
mentionNotifications: true,
tabs: true,
urlLinkify: true,
pinnedMessages: true,
searchMessages: true,
// No presence, read receipts, or unread counts in room mode
presence: false,
readReceipts: false,
unreadCounts: false,
},
// Callbacks map to app's own IPC handlers
onFetchMessages: async (roomType) => { /* fetch from app DB */ },
onSendMessage: async (roomType, content, attachment, replyTo) => { /* insert into app DB */ },
onUploadAttachment: async (attachment) => { /* save to network share */ },
onFetchMentionUsers: async (conversationId) => { /* return app's users */ },
onPinMessage: async (msg, conversationId) => { /* pin/unpin in app DB */ },
onUnpinMessage: async (msg, conversationId) => { /* unpin in app DB */ },
onFetchPinnedMessages: async (conversationId) => { /* return pinned messages */ },
});File Attachments on Network Share
Room-mode apps typically store attachments on a shared network drive. The app handles all file I/O -- the module only provides the UI:
onUploadAttachment: async (attachment) => {
// attachment = { name, type, size, path, file, data }
// 'path' is set for file picker / drag-drop
// 'data' (base64) is set for clipboard paste
const result = await ipcInvoke('upload-chat-attachment', {
name: attachment.name,
type: attachment.type,
sourcePath: attachment.path,
data: attachment.data
});
return result; // { success, path, name, type, size }
}Polling vs Socket
Room mode uses polling (messagePollingMs) instead of Socket.IO. The module automatically polls for new messages at the configured interval with silent refresh -- it only re-renders when messages have actually changed, and preserves the user's scroll position. Apps that have Socket.IO can also push events via chat.onNewMessage() for instant updates.
33. Security & Public Module Guidelines
The chat module is designed to be publicly distributable with no sensitive information:
- No credentials -- no database passwords, connection strings, or API keys
- No network paths -- no IP addresses, UNC paths, or server URLs
- No SQL -- no raw queries, table names, or schema definitions in the module code
- No app-specific logic -- the module is completely app-agnostic
- All sensitive config is passed at runtime by the app via callbacks and config objects
- The module never connects to any network -- apps handle all I/O through callbacks
