npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

  1. Overview
  2. File Structure
  3. Installation & Quick Start
  4. Modes
  5. Configuration & Callbacks
  6. Features
  7. File Attachments
  8. Reply System
  9. Files Panel
  10. Pinned Messages
  11. In-Chat Search
  12. Confirm Dialog
  13. Resizable Sidebar
  14. Responsive Design
  15. Context Menus
  16. Configurable Labels
  17. Universal Data Formats
  18. Frontend Components
  19. CSS Theming
  20. Built-in Themes (30)
  21. Backend API Factory
  22. Real-Time Layer
  23. Presence System
  24. Unread Counts & Read Receipts
  25. @Mentions & Notifications
  26. User Sync / Registration
  27. Public API
  28. IPC Channel Reference
  29. Database Schema
  30. Test App
  31. Integration Guide -- Conversation Mode
  32. Integration Guide -- Room Mode
  33. 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 mode config
  • 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 highlighting

3. Installation & Quick Start

npm install tacel-chat
const { 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

  1. User selects file (drag-drop or picker)
  2. Preview bar appears above input
  3. User types message (optional) and clicks Send
  4. Module calls onUploadAttachment({ name, type, size, path, file }) > app copies file to storage, returns { success, path, name, type, size }
  5. Module calls onSendMessage(conversationId, content, uploadedAttachment, replyTo) > app stores message with attachment data
  6. 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:

  1. The messages area smoothly scrolls to center the original message
  2. The original message gets a 1.5s flash highlight animation (accent color fade)
  3. Each message has a data-msg-id attribute 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's onPinMessage callback 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 onUnpinMessage callback

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-chat container so all CSS variables cascade
  • Exported from index.js so 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-resize during 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 input
  • createPreviewBar() -- preview bar above input (icon + name + size + x remove)
  • createDropZone(messagesEl) -- drag-and-drop overlay on messages area
  • consumeAttachment() -- get selected file and clear
  • renderAttachmentPreview(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

  1. On init: onPresenceHeartbeat() immediately
  2. Every heartbeatMs: onPresenceHeartbeat()
  3. On beforeunload: onPresenceOffline()
  4. Rendering: check lastSeen against presenceStaleMs

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:

  1. Each app has its own users table with its own user IDs
  2. The shared chat database has a chat_users table with a global_key column
  3. global_key = username.trim().toLowerCase() -- this is the cross-app identity key
  4. When a user logs in, the app calls syncUser({ original_id, app, username })
  5. 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_users row

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 Server on a configurable port (default 3001)
  • Listens on 0.0.0.0 so all machines on the network can connect
  • Relays three events to all connected clients:
    • chat:new_message -- new message sent
    • chat:read -- conversation marked as read
    • chat:new_conversation -- new conversation created

Socket Client (all app instances connect):

  • Reads socketServerUrl from version.json (e.g. http://192.168.x.x:3001)
  • Socket.IO client with autoConnect: true
  • Exports getSocket() and emit(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

  1. On login: syncUser() sets is_online = 1
  2. Every 5s: onPresenceHeartbeat() > UPDATE chat_users SET is_online = 1, last_seen = NOW() WHERE global_key = ?
  3. On beforeunload: onPresenceOffline() > UPDATE chat_users SET is_online = 0, last_seen = NOW() WHERE global_key = ?
  4. Module checks last_seen against presenceStaleMs to 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_reads rows 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 info

Room 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