tacel-chat 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1435 @@
1
+ # Tacel Chat Module
2
+
3
+ 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.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [Overview](#1-overview)
10
+ 2. [File Structure](#2-file-structure)
11
+ 3. [Installation & Quick Start](#3-installation--quick-start)
12
+ 4. [Modes](#4-modes)
13
+ 5. [Configuration & Callbacks](#5-configuration--callbacks)
14
+ 6. [Features](#6-features)
15
+ 7. [File Attachments](#7-file-attachments)
16
+ 8. [Reply System](#8-reply-system)
17
+ 9. [Files Panel](#9-files-panel)
18
+ 10. [Pinned Messages](#10-pinned-messages)
19
+ 11. [In-Chat Search](#11-in-chat-search)
20
+ 12. [Confirm Dialog](#12-confirm-dialog)
21
+ 13. [Resizable Sidebar](#13-resizable-sidebar)
22
+ 14. [Responsive Design](#14-responsive-design)
23
+ 15. [Context Menus](#15-context-menus)
24
+ 16. [Configurable Labels](#16-configurable-labels)
25
+ 17. [Universal Data Formats](#17-universal-data-formats)
26
+ 18. [Frontend Components](#18-frontend-components)
27
+ 19. [CSS Theming](#19-css-theming)
28
+ 20. [Built-in Themes (30)](#20-built-in-themes-30)
29
+ 21. [Backend API Factory](#21-backend-api-factory)
30
+ 22. [Real-Time Layer](#22-real-time-layer)
31
+ 23. [Presence System](#23-presence-system)
32
+ 24. [Unread Counts & Read Receipts](#24-unread-counts--read-receipts)
33
+ 25. [@Mentions & Notifications](#25-mentions--notifications)
34
+ 26. [User Sync / Registration](#26-user-sync--registration)
35
+ 27. [Public API](#27-public-api)
36
+ 28. [IPC Channel Reference](#28-ipc-channel-reference)
37
+ 29. [Database Schema](#29-database-schema)
38
+ 30. [Test App](#30-test-app)
39
+ 31. [Integration Guide -- Conversation Mode](#31-integration-guide--conversation-mode)
40
+ 32. [Integration Guide -- Room Mode](#32-integration-guide--room-mode)
41
+ 33. [Security & Public Module Guidelines](#33-security--public-module-guidelines)
42
+
43
+ ---
44
+
45
+ ## 1. Overview
46
+
47
+ 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:
48
+
49
+ - **Conversation mode** -- sidebar with direct messages + team conversations, presence, read receipts, seen indicators (used by Electron-template, Tech-Portal, ShipWorks, Office-HQ, Admin-Pro)
50
+ - **Room mode** -- tabbed chat rooms with polling, file attachments, @mentions, notifications (used by Wire-Scheduler)
51
+
52
+ ### Key Features
53
+
54
+ - **Two modes** -- conversation-based or room-based via `mode` config
55
+ - **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
56
+ - **Files panel** -- collapsible sidebar listing all files in the current chat with click-to-scroll, open, right-click context menu
57
+ - **Reply system** -- right-click > Reply, accent-striped reply bar above input, clickable reply references on messages with scroll-to-original + highlight animation
58
+ - **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
59
+ - **In-chat search** -- keyword search within current conversation/room, highlighted matching text, click result to scroll to message
60
+ - **Confirm dialog** -- themed in-app modal replacing `window.alert()`/`window.confirm()`, inherits theme CSS variables
61
+ - **Resizable sidebar** -- drag handle on sidebar edge, 200-500px range, configurable via `features.resizableSidebar`
62
+ - **Responsive design** -- adapts to small screens (700px, 520px, 380px breakpoints), stacked layout, overlay panels
63
+ - **Silent refresh** -- polling re-renders only when messages change, preserves scroll position
64
+ - **Context-aware @mentions** -- mention list filtered to participants of the active conversation/room
65
+ - **Date separators** -- "Today", "Yesterday", and full date labels between message groups
66
+ - **Context menus** -- right-click on messages, conversations, tabs, files, users with fully configurable items
67
+ - **Configurable labels** -- every UI text string is overridable for localization or customization
68
+ - **@Mentions** -- `@` trigger, filtered user popup, keyboard navigation, highlight rendering
69
+ - **Presence** -- online/offline dots, heartbeat, "Last seen" text
70
+ - **Read receipts** -- per-message seen indicators with "checkmark Seen" tooltip
71
+ - **Unread counts** -- badge on conversations/tabs
72
+ - **30 built-in themes** -- light, dark, and app-specific presets, all via CSS variables
73
+ - **Backend API factory** -- optional IPC handler registration using app-provided DB functions
74
+ - **User sync utility** -- cross-app user registration for shared APP-CHATS database
75
+ - **Zero app-specific code** -- module knows nothing about IPC, DB schemas, or network paths
76
+
77
+ ---
78
+
79
+ ## 2. File Structure
80
+
81
+ ```
82
+ chat-module/
83
+ |-- index.js # Entry: exports { TacelChat, initChatAPI, syncUser, themes, ConfirmDialog }
84
+ |-- package.json # npm: tacel-chat
85
+ |-- README.md # This file
86
+ |-- chat.js # Main frontend: init, state, public API, send chain
87
+ |-- chat.css # All styles with CSS variables (~2500 lines)
88
+ |-- themes.js # 30 built-in theme presets
89
+ |-- chat-api.js # Backend IPC handler factory (optional)
90
+ |-- chat-sync.js # User sync for APP-CHATS registration
91
+ |-- components/
92
+ | |-- sidebar.js # Conversation list (conversation mode) + resizable drag handle
93
+ | |-- tabs.js # Tab bar (room mode) + actions area for panel toggles
94
+ | |-- message-area.js # Message display + input + reply bar + drop zone + panel orchestration
95
+ | |-- message.js # Single message rendering (bubble + card modes) + pin icon
96
+ | |-- attachment.js # File attachment system (drag-drop, picker, preview bar, card rendering)
97
+ | |-- files-panel.js # Files panel sidebar (lists all files in current chat)
98
+ | |-- pinned-panel.js # Pinned messages panel (lists pinned messages, click-to-scroll, unpin)
99
+ | |-- search-panel.js # In-chat search panel (keyword search, highlighted results, click-to-scroll)
100
+ | |-- confirm.js # Themed confirm/alert dialog (replaces window.alert/confirm)
101
+ | |-- context-menu.js # Right-click context menus (messages, conversations, tabs, files, users)
102
+ | |-- new-chat.js # New conversation modal
103
+ | |-- mention.js # @mention popup
104
+ | |-- presence.js # Presence helpers
105
+ |-- utils/
106
+ |-- format.js # Time formatting, date grouping, date dividers
107
+ |-- dom.js # DOM helpers, escape HTML, scroll utilities
108
+ |-- linkify.js # URL detection, mention highlighting
109
+ ```
110
+
111
+ ---
112
+
113
+ ## 3. Installation & Quick Start
114
+
115
+ ```bash
116
+ npm install tacel-chat
117
+ ```
118
+
119
+ ```js
120
+ const { TacelChat } = require('tacel-chat');
121
+
122
+ const chat = new TacelChat();
123
+ chat.initialize(document.getElementById('chat-container'), {
124
+ mode: 'conversation',
125
+ currentUsername: 'pierre',
126
+ theme: 'shipworks',
127
+ features: {
128
+ directMessages: true,
129
+ teamConversations: true,
130
+ presence: true,
131
+ readReceipts: true,
132
+ unreadCounts: true,
133
+ search: true,
134
+ newChat: true,
135
+ attachments: true,
136
+ attachmentPreview: true,
137
+ },
138
+ onFetchUsers: async () => { /* return users */ },
139
+ onFetchConversations: async (username) => { /* return conversations */ },
140
+ onFetchMessages: async (conversationId) => { /* return messages */ },
141
+ onSendMessage: async (conversationId, content, attachment, replyTo) => { /* send */ },
142
+ onUploadAttachment: async (attachment) => { /* upload, return { success, path, name, type, size } */ },
143
+ onOpenFile: async (filePath) => { /* open file */ },
144
+ onOpenFolder: async (filePath) => { /* show in folder */ },
145
+ // ... more callbacks
146
+ });
147
+ ```
148
+
149
+ ```html
150
+ <link rel="stylesheet" href="./node_modules/tacel-chat/chat.css">
151
+ ```
152
+
153
+ ---
154
+
155
+ ## 4. Modes
156
+
157
+ ### Conversation Mode (`mode: 'conversation'`)
158
+ 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.
159
+
160
+ ### Room Mode (`mode: 'room'`)
161
+ 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.
162
+
163
+ ---
164
+
165
+ ## 5. Configuration & Callbacks
166
+
167
+ ```js
168
+ chat.initialize(container, {
169
+ mode: 'conversation', // 'conversation' or 'room'
170
+ currentUsername: 'pierre',
171
+
172
+ features: {
173
+ directMessages: true,
174
+ teamConversations: true,
175
+ teamManagement: false, // Admin-Pro only
176
+ presence: true,
177
+ readReceipts: true,
178
+ unreadCounts: true,
179
+ search: true,
180
+ newChat: true,
181
+ attachments: true, // Enable file attachments (drag-drop + picker)
182
+ attachmentPreview: true, // Enable inline image/PDF previews
183
+ mentions: false, // Enable @mentions (room mode)
184
+ mentionNotifications: false,
185
+ urlLinkify: true,
186
+ tabs: false, // Tab bar (room mode)
187
+ darkMode: false,
188
+ resizableSidebar: true, // Drag-to-resize sidebar (conversation mode)
189
+ pinnedMessages: true, // Pinned messages panel
190
+ searchMessages: true, // In-chat search panel
191
+ },
192
+
193
+ // Room mode config
194
+ rooms: [
195
+ { type: 'general', name: 'General', icon: 'fas fa-comments' },
196
+ { type: 'office', name: 'Office', icon: 'fas fa-building' },
197
+ ],
198
+ defaultRoom: 'general',
199
+
200
+ // Timing
201
+ refreshIntervalMs: 5000,
202
+ heartbeatMs: 5000,
203
+ presenceStaleMs: 10000,
204
+ messagePollingMs: 0, // Room mode polling (Wire-Scheduler uses 2000)
205
+
206
+ // --- Data callbacks ---
207
+ onFetchUsers: async () => [],
208
+ onFetchConversations: async (currentUsername) => [],
209
+ onFetchMessages: async (conversationId) => [],
210
+ onSendMessage: async (conversationId, content, attachment, replyTo) => {},
211
+ onStartDirect: async (currentUsername, otherUserId) => {},
212
+ onFetchUnreadCounts: async (currentUsername) => [],
213
+ onMarkRead: async (conversationId, currentUsername) => {},
214
+ onFetchMessageReads: async (conversationId) => [],
215
+
216
+ // --- Presence callbacks ---
217
+ onPresenceHeartbeat: async (currentUsername) => {},
218
+ onPresenceOffline: async (currentUsername) => {},
219
+ onFetchDirectPeers: async (currentUsername) => [],
220
+
221
+ // --- Team management (optional) ---
222
+ onCreateTeam: async (currentUsername, name, memberIds) => {},
223
+ onUpdateTeam: async (conversationId, name, memberIds, currentUsername) => {},
224
+ onDeleteTeam: async (conversationId) => {},
225
+ onListTeams: async () => [],
226
+
227
+ // --- Attachment callbacks ---
228
+ onUploadAttachment: async (attachment) => {}, // { name, type, size, path, file }
229
+ onOpenFile: async (filePath) => {},
230
+ onOpenFolder: async (filePath) => {}, // Show file in folder (shell.showItemInFolder)
231
+ onGetFileContent: async (filePath) => {}, // Return base64 for image/PDF preview
232
+
233
+ // --- Mention callbacks ---
234
+ onFetchMentionUsers: async (conversationId) => [], // Context-aware: receives active conversation ID
235
+ onMarkRoomNotificationsRead: async (roomId, userId) => {},
236
+
237
+ // --- Pinned message callbacks ---
238
+ onPinMessage: async (msg, conversationId) => {}, // Pin or unpin a message
239
+ onUnpinMessage: async (msg, conversationId) => {}, // Unpin a message from the panel
240
+ onFetchPinnedMessages: async (conversationId) => [], // Return [{ id, content, senderName, timestamp }]
241
+
242
+ // --- Context menu action callbacks ---
243
+ onDeleteMessage: (msg) => {},
244
+ onQuoteMessage: (msg) => {},
245
+ onForwardMessage: (msg) => {},
246
+ onEditMessage: (msg) => {},
247
+
248
+ // --- Other callbacks ---
249
+ onLinkClick: (url) => {},
250
+
251
+ // --- Theme ---
252
+ theme: 'shipworks', // String preset name or object of CSS variables
253
+
254
+ // --- Labels (all overridable) ---
255
+ labels: { /* see Section 16 */ },
256
+ });
257
+ ```
258
+
259
+ ---
260
+
261
+ ## 6. Features
262
+
263
+ All features are opt-in via the `features` config object:
264
+
265
+ | Feature | Default | Description |
266
+ |---------|---------|-------------|
267
+ | `directMessages` | `true` | Show direct message conversations in sidebar |
268
+ | `teamConversations` | `true` | Show team conversations in sidebar |
269
+ | `teamManagement` | `false` | Enable create/edit/delete team (Admin-Pro) |
270
+ | `presence` | `true` | Online/offline dots and status text |
271
+ | `readReceipts` | `true` | "checkmark Seen" on last own message |
272
+ | `unreadCounts` | `true` | Unread badges on conversations |
273
+ | `search` | `true` | Search bar in sidebar |
274
+ | `newChat` | `true` | "+" button to start new conversation |
275
+ | `attachments` | `false` | File attachments (drag-drop + picker + preview) |
276
+ | `attachmentPreview` | `false` | Inline image/PDF previews on messages |
277
+ | `mentions` | `false` | @mention autocomplete popup |
278
+ | `mentionNotifications` | `false` | Backend mention notification processing |
279
+ | `urlLinkify` | `true` | Auto-detect and linkify URLs in messages |
280
+ | `tabs` | `false` | Tab bar for room mode |
281
+ | `darkMode` | `false` | Dark mode support |
282
+ | `resizableSidebar` | `true` | Drag-to-resize sidebar in conversation mode |
283
+ | `pinnedMessages` | `true` | Pinned messages panel with toggle button |
284
+ | `searchMessages` | `true` | In-chat search panel with toggle button |
285
+
286
+ ---
287
+
288
+ ## 7. File Attachments
289
+
290
+ Enabled via `features.attachments: true`. Supports **any file type and any file size** -- the implementing app handles storage (e.g. copy to shared drive).
291
+
292
+ ### Drag-and-Drop
293
+ Drag a file onto the message area > a dashed overlay appears with "Drop file to attach" > release to select the file.
294
+
295
+ ### Clipboard Paste
296
+ 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.
297
+
298
+ 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.
299
+
300
+ ### File Picker
301
+ Click the paperclip button in the input area > standard file picker opens > select any file.
302
+
303
+ ### Attachment Preview Bar
304
+ When a file is selected (via drag-drop, picker, or clipboard paste), a preview bar appears above the input showing:
305
+ - **File type icon** (color-coded by type: image, PDF, Word, Excel, archive, code, audio, video, etc.)
306
+ - **File name** (truncated with ellipsis)
307
+ - **File size** (formatted: KB, MB, GB)
308
+ - **x button** to remove the attachment
309
+
310
+ ### File-Only Messages
311
+ 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.
312
+
313
+ ### Attachment Card (on messages)
314
+ When a message has an attachment, it renders as a card with:
315
+ - **Inline preview** for images (max 200px height) and PDFs (160px embed) when `attachmentPreview: true`
316
+ - **File info row** with type icon, file name, file size
317
+ - **Open button** (opens the file in the default app)
318
+ - **Open Folder button** (shows the file in its folder via `shell.showItemInFolder`)
319
+
320
+ ### Upload Flow
321
+ 1. User selects file (drag-drop or picker)
322
+ 2. Preview bar appears above input
323
+ 3. User types message (optional) and clicks Send
324
+ 4. Module calls `onUploadAttachment({ name, type, size, path, file })` > app copies file to storage, returns `{ success, path, name, type, size }`
325
+ 5. Module calls `onSendMessage(conversationId, content, uploadedAttachment, replyTo)` > app stores message with attachment data
326
+ 6. Messages reload, attachment card renders on the message
327
+
328
+ ### File Icon Mapping
329
+ The module maps file extensions to FontAwesome icons:
330
+ - **Images** -- jpg, png, gif, bmp, webp, svg > `fa-file-image`
331
+ - **PDF** > `fa-file-pdf`
332
+ - **Word** -- doc, docx, odt, rtf > `fa-file-word`
333
+ - **Excel** -- xls, xlsx, csv, ods > `fa-file-excel`
334
+ - **PowerPoint** -- ppt, pptx > `fa-file-powerpoint`
335
+ - **Archives** -- zip, rar, 7z, tar, gz > `fa-file-archive`
336
+ - **Code** -- js, py, html, css, json, sql, etc. > `fa-file-code`
337
+ - **Audio** -- mp3, wav, ogg, flac > `fa-file-audio`
338
+ - **Video** -- mp4, avi, mkv, mov > `fa-file-video`
339
+ - **Text** -- txt, log, md, ini > `fa-file-alt`
340
+ - **Other** > `fa-file`
341
+
342
+ ---
343
+
344
+ ## 8. Reply System
345
+
346
+ Right-click any message > **Reply** > a reply bar appears above the input area.
347
+
348
+ ### Reply Bar (above input)
349
+ - **4px accent stripe** on the left edge
350
+ - **Header row**: reply icon + "Replying to" label + **sender name** in accent color
351
+ - **Message preview**: first 140 characters of the original message
352
+ - **x close button** on the right (turns red on hover)
353
+ - Animated slide-in
354
+
355
+ ### Reply Reference (on messages)
356
+ When a message is a reply, it shows a reference card above the message:
357
+ - **3px accent left border** with tinted background
358
+ - **Sender name** with reply icon
359
+ - **Message preview** (first 120 characters)
360
+ - **Clickable** -- clicking scrolls to and highlights the original message
361
+
362
+ ### Scroll-to-Original
363
+ When you click a reply reference:
364
+ 1. The messages area smoothly scrolls to center the original message
365
+ 2. The original message gets a **1.5s flash highlight animation** (accent color fade)
366
+ 3. Each message has a `data-msg-id` attribute for targeting
367
+
368
+ ### Reply Data Flow
369
+ `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.
370
+
371
+ ---
372
+
373
+ ## 9. Files Panel
374
+
375
+ A collapsible panel on the right side of the message area that lists **all file attachments** in the current conversation/room.
376
+
377
+ ### Toggle
378
+ - **Folder icon button** in the chat header (conversation mode) or tab bar (room mode)
379
+ - **Badge** showing the count of files in the current chat
380
+ - Click to open/close the panel
381
+ - Active state: accent border + tinted background
382
+
383
+ ### Panel Contents
384
+ - **Header**: "Files" title with folder icon + close button
385
+ - **File list** (newest first): each item shows:
386
+ - File type icon (color-coded)
387
+ - File name (truncated)
388
+ - Meta line: file size . sender name . time
389
+ - **Open** and **Open Folder** buttons (appear on hover)
390
+ - **Empty state**: folder icon + "No files shared yet"
391
+
392
+ ### Interactions
393
+ - **Click** a file item > closes the panel and scrolls to the message containing that file (with highlight animation)
394
+ - **Right-click** a file item > context menu with: Open File, Open Folder, Scroll to Message
395
+ - **Open/Open Folder buttons** on hover > direct file actions
396
+
397
+ ---
398
+
399
+ ## 10. Pinned Messages
400
+
401
+ Pin important messages to make them easily accessible. Controlled by the app via callbacks.
402
+
403
+ ### Pinning a Message
404
+ - Right-click a message > **Pin Message** (or **Unpin Message** if already pinned)
405
+ - The action routes through `chat.pinMessage(msg)` which calls the app's `onPinMessage` callback with the message and conversation ID
406
+ - After pinning, the pinned panel reloads and messages re-render to show pin icons
407
+
408
+ ### Pin Icon on Messages
409
+ Pinned messages display a small thumbtack icon in the message header (between sender name and timestamp), styled in the accent color.
410
+
411
+ ### Pinned Panel
412
+ - **Thumbtack icon button** in the chat header (conversation mode) or tab bar (room mode)
413
+ - **Badge** showing the count of pinned messages
414
+ - Click to open/close the panel
415
+
416
+ ### Panel Contents
417
+ - **Header**: "Pinned Messages" title with pin icon + close button
418
+ - **Pinned message list**: each item shows:
419
+ - Sender name
420
+ - Message preview (2-line clamp)
421
+ - Timestamp
422
+ - **x unpin button** on hover
423
+ - **Empty state**: pin icon + "No pinned messages"
424
+
425
+ ### Interactions
426
+ - **Click** a pinned item > closes the panel and scrolls to the original message (with highlight animation)
427
+ - **x button** on hover > unpins the message via `onUnpinMessage` callback
428
+
429
+ ### Callbacks
430
+ ```js
431
+ onPinMessage: async (msg, conversationId) => {
432
+ // msg.isPinned tells you current state -- toggle accordingly
433
+ // App handles the actual pin/unpin in its database
434
+ },
435
+ onUnpinMessage: async (msg, conversationId) => {
436
+ // Called when user clicks x on a pinned panel item
437
+ },
438
+ onFetchPinnedMessages: async (conversationId) => {
439
+ // Return array of { id, content, senderName, timestamp }
440
+ return [];
441
+ },
442
+ ```
443
+
444
+ ---
445
+
446
+ ## 11. In-Chat Search
447
+
448
+ Search within the current conversation or room for keywords.
449
+
450
+ ### Toggle
451
+ - **Search icon button** in the chat header (conversation mode) or tab bar (room mode)
452
+ - Click to open/close the search panel
453
+
454
+ ### Panel Contents
455
+ - **Header**: "Search Messages" title with search icon + close button
456
+ - **Search input**: auto-focuses when panel opens, real-time filtering as you type
457
+ - **Result count**: "X results" displayed below the input
458
+ - **Result list**: each item shows:
459
+ - Sender name
460
+ - Message text with **highlighted matching keywords** (`<mark>` tags)
461
+ - Timestamp
462
+ - **Empty state**: search icon + "Search for messages"
463
+ - **No results state**: "No messages found"
464
+
465
+ ### Interactions
466
+ - **Type** in the search input > results filter in real-time
467
+ - **Click** a result > closes the panel and scrolls to the original message (with highlight animation)
468
+
469
+ ### Data Flow
470
+ 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.
471
+
472
+ ---
473
+
474
+ ## 12. Confirm Dialog
475
+
476
+ 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.
477
+
478
+ ### Usage
479
+ ```js
480
+ const { ConfirmDialog } = require('tacel-chat');
481
+
482
+ const dialog = new ConfirmDialog(mountElement); // Mount inside .tacel-chat for theme inheritance
483
+
484
+ // Alert (single OK button)
485
+ await dialog.alert('Message deleted successfully', {
486
+ title: 'Deleted',
487
+ icon: 'fas fa-trash',
488
+ buttonText: 'OK'
489
+ });
490
+
491
+ // Confirm (OK + Cancel)
492
+ const confirmed = await dialog.confirm('Are you sure you want to pin this message?', {
493
+ title: 'Pin Message',
494
+ icon: 'fas fa-thumbtack',
495
+ confirmText: 'Pin',
496
+ cancelText: 'Cancel'
497
+ });
498
+ if (confirmed) { /* user clicked Pin */ }
499
+ ```
500
+
501
+ ### Features
502
+ - **Dark semi-transparent backdrop** with blur effect
503
+ - **Pop-in animation** (scale + fade)
504
+ - **Icon** (FontAwesome) + **title** + **message**
505
+ - **Primary/secondary buttons** with accent color styling
506
+ - **Click outside** to dismiss (resolves as cancel)
507
+ - **Inherits theme** -- mounts inside the `.tacel-chat` container so all CSS variables cascade
508
+ - **Exported** from `index.js` so apps can create their own instances
509
+
510
+ ---
511
+
512
+ ## 13. Resizable Sidebar
513
+
514
+ The conversation list sidebar can be resized by dragging its right edge.
515
+
516
+ ### Behavior
517
+ - A thin **6px drag handle** on the right edge of the sidebar
518
+ - **Highlights** with accent color on hover and during drag
519
+ - **Min width**: 200px
520
+ - **Max width**: 500px
521
+ - Cursor changes to `col-resize` during drag
522
+ - Grid layout updates in real-time as you drag
523
+
524
+ ### Configuration
525
+ ```js
526
+ features: {
527
+ resizableSidebar: true, // Default: true. Set to false to disable.
528
+ }
529
+ ```
530
+
531
+ ### Cleanup
532
+ Event listeners are properly cleaned up on `destroy()`.
533
+
534
+ ---
535
+
536
+ ## 14. Responsive Design
537
+
538
+ The chat module adapts to small container/window sizes via CSS media queries.
539
+
540
+ ### Breakpoints
541
+
542
+ | Breakpoint | Changes |
543
+ |------------|---------|
544
+ | <= 700px | Sidebar narrows to 220px, side panels shrink to 200px, modals cap at 90vw |
545
+ | <= 520px | Sidebar stacks vertically (full width, max 40vh), resize handle hidden, side panels overlay as absolute drawers with shadow, tabs/topbar compact |
546
+ | <= 380px | Ultra-compact: smaller header padding, 28px action buttons, tighter gaps |
547
+
548
+ ### Panel Behavior at Small Sizes
549
+ 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.
550
+
551
+ ### Confirm Dialog
552
+ The confirm dialog scales down at each breakpoint with smaller padding, font sizes, and button sizes.
553
+
554
+ ---
555
+
556
+ ## 15. Context Menus
557
+
558
+ Right-click context menus are available on messages, conversations, tabs, files, and users. All menu item labels are configurable via `labels`.
559
+
560
+ ### Message Context Menu
561
+ - **Copy Text** -- copies message content
562
+ - **Reply** -- sets the reply bar (built-in, not a callback)
563
+ - **Quote** -- inserts quoted text into input
564
+ - **Forward**, **Edit Message**, **Delete Message** -- via callbacks
565
+ - **Pin/Unpin Message** -- routes through `chat.pinMessage()` for proper panel reload
566
+
567
+ ### Conversation Context Menu
568
+ - **Mark as Read**, **Mute/Unmute Notifications**, **Pin/Unpin to Top**
569
+ - **Leave Conversation**, **Delete Conversation**
570
+
571
+ ### File Context Menu (in Files Panel)
572
+ - **Open File** -- opens in default app
573
+ - **Open Folder** -- shows in file explorer
574
+ - **Scroll to Message** -- scrolls to and highlights the message
575
+
576
+ ### Tab Context Menu (room mode)
577
+ - **Open Room**, **Refresh Messages**, **Search Messages**, **Clear Chat History**
578
+
579
+ ---
580
+
581
+ ## 16. Configurable Labels
582
+
583
+ Every UI text string is overridable via the `labels` config object. This enables localization or custom wording.
584
+
585
+ ```js
586
+ labels: {
587
+ sidebarTitle: 'Messages',
588
+ searchPlaceholder: 'Search conversations...',
589
+ newChatTitle: 'New Direct Message',
590
+ newChatSearch: 'Search users...',
591
+ teamsHeader: 'Teams',
592
+ directsHeader: 'Direct Messages',
593
+ noConversations: 'No conversations yet',
594
+ noResults: 'No conversations found',
595
+ noMessages: 'No messages yet',
596
+ noMessagesHint: 'Send a message to start the conversation',
597
+ inputPlaceholder: 'Type a message...',
598
+ roomTitle: 'Chat Center',
599
+ loadingText: 'Loading...',
600
+ errorText: 'Failed to load chat',
601
+ retryText: 'Retry',
602
+ seenBy: 'Seen by',
603
+ online: 'Online',
604
+ offline: 'Offline',
605
+ lastSeen: 'Last seen',
606
+ typing: 'typing...',
607
+ noUsers: 'No users available',
608
+ // Files panel
609
+ filesPanel: 'Files',
610
+ noFiles: 'No files shared yet',
611
+ openFile: 'Open File',
612
+ openFolder: 'Open Folder',
613
+ scrollToMessage: 'Scroll to Message',
614
+ // Pinned panel
615
+ pinnedPanel: 'Pinned Messages',
616
+ noPinned: 'No pinned messages',
617
+ // Search panel
618
+ searchPanel: 'Search Messages',
619
+ searchPlaceholderMessages: 'Search messages...',
620
+ noSearchResults: 'No messages found',
621
+ // Context menu labels
622
+ copyText: 'Copy Text',
623
+ reply: 'Reply',
624
+ quote: 'Quote',
625
+ forward: 'Forward',
626
+ editMessage: 'Edit Message',
627
+ deleteMessage: 'Delete Message',
628
+ pinMessage: 'Pin Message',
629
+ unpinMessage: 'Unpin Message',
630
+ markAsRead: 'Mark as Read',
631
+ muteNotifications: 'Mute Notifications',
632
+ unmuteNotifications: 'Unmute',
633
+ pinToTop: 'Pin to Top',
634
+ unpin: 'Unpin',
635
+ leaveConversation: 'Leave Conversation',
636
+ deleteConversation: 'Delete Conversation',
637
+ refreshMessages: 'Refresh Messages',
638
+ searchMessages: 'Search Messages',
639
+ clearChat: 'Clear Chat History',
640
+ openRoom: 'Open Room',
641
+ sendDirectMessage: 'Send Direct Message',
642
+ viewProfile: 'View Profile',
643
+ }
644
+ ```
645
+
646
+ ---
647
+
648
+ ## 17. Universal Data Formats
649
+
650
+ ### Message
651
+ ```js
652
+ {
653
+ id: number|string,
654
+ conversationId: number|string,
655
+ senderId: number|string,
656
+ senderName: string,
657
+ content: string,
658
+ timestamp: Date|string,
659
+ isOwn: boolean,
660
+ isPinned: boolean, // Set automatically by the module based on pinned panel data
661
+ hasAttachment: boolean,
662
+ attachmentName: string|null,
663
+ attachmentPath: string|null,
664
+ attachmentType: string|null,
665
+ attachmentSize: number|null,
666
+ seenBy: Array<{ userId, username, readAt }>|null,
667
+ replyTo: { id, senderName, content }|null,
668
+ meta: object
669
+ }
670
+ ```
671
+
672
+ ### Conversation
673
+ ```js
674
+ {
675
+ id: number|string,
676
+ name: string,
677
+ type: 'direct'|'team'|'room',
678
+ lastMessage: string|null,
679
+ lastMessageTime: Date|string|null,
680
+ unreadCount: number,
681
+ participants: Array<string>,
682
+ meta: object
683
+ }
684
+ ```
685
+
686
+ ### User
687
+ ```js
688
+ {
689
+ id: number|string,
690
+ username: string,
691
+ isOnline: boolean,
692
+ lastSeen: Date|string|null,
693
+ app: string|null,
694
+ meta: object
695
+ }
696
+ ```
697
+
698
+ ---
699
+
700
+ ## 18. Frontend Components
701
+
702
+ ### Sidebar (`sidebar.js`) -- Conversation mode
703
+ 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).
704
+
705
+ ### Tabs (`tabs.js`) -- Room mode
706
+ 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.
707
+
708
+ ### Message Area (`message-area.js`)
709
+ 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.
710
+
711
+ ### Message (`message.js`)
712
+ - **Conversation mode**: Avatar (rounded square), sender + pin icon + time header, bubble (accent for own, neutral for others), attachment card, seen indicator
713
+ - **Room mode**: Avatar (circle) always left, sender + pin icon + time, card-style content, attachment card, highlighted mentions + URLs
714
+ - Both modes: reply reference above message (clickable, scrolls to original), pin icon on pinned messages
715
+
716
+ ### Attachment (`attachment.js`)
717
+ Complete file attachment system:
718
+ - `createControls()` -- paperclip button + hidden file input
719
+ - `createPreviewBar()` -- preview bar above input (icon + name + size + x remove)
720
+ - `createDropZone(messagesEl)` -- drag-and-drop overlay on messages area
721
+ - `consumeAttachment()` -- get selected file and clear
722
+ - `renderAttachmentPreview(msg, container)` -- render attachment card on a message
723
+ - Exports: `getFileIcon(filename)`, `formatFileSize(bytes)`
724
+
725
+ ### Files Panel (`files-panel.js`)
726
+ 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.
727
+
728
+ ### Pinned Panel (`pinned-panel.js`)
729
+ 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.
730
+
731
+ ### Search Panel (`search-panel.js`)
732
+ Collapsible right-side panel with search input. Real-time keyword filtering, highlighted matching text, result count. Click result to scroll to original message.
733
+
734
+ ### Confirm Dialog (`confirm.js`)
735
+ 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`.
736
+
737
+ ### Context Menu (`context-menu.js`)
738
+ Positioned context menu with icon + label items. Supports: separators, headers, disabled items, danger items, hidden items. Target types: message, conversation, tab, background, user, file.
739
+
740
+ ### New Chat Modal (`new-chat.js`)
741
+ Overlay, search input, user list (excludes self + existing direct peers), click > create conversation.
742
+
743
+ ### @Mention Popup (`mention.js`)
744
+ Triggered by `@`, filtered user list (context-aware -- filtered to conversation/room participants), keyboard navigation (arrows/tab/enter/escape), inserts `@username `.
745
+
746
+ ### Presence (`presence.js`)
747
+ Helpers: `checkOnline(user, staleMs)`, `getPresenceText(user, staleMs)` -- returns "Online" or "Last seen X ago".
748
+
749
+ ---
750
+
751
+ ## 19. CSS Theming
752
+
753
+ ### Theme System
754
+
755
+ Themes can be applied three ways:
756
+
757
+ ```js
758
+ // 1. Built-in preset by name
759
+ chat.initialize(container, { theme: 'dark' });
760
+
761
+ // 2. Custom CSS variables object
762
+ chat.initialize(container, { theme: { '--chat-bg': '#111', '--chat-accent': '#ff6600' } });
763
+
764
+ // 3. Extend a preset
765
+ const { themes } = require('tacel-chat');
766
+ chat.initialize(container, { theme: { ...themes.dark, '--chat-accent': '#ff6600' } });
767
+ ```
768
+
769
+ ### CSS Variables Reference
770
+
771
+ | Variable | Description |
772
+ |----------|-------------|
773
+ | `--chat-bg` | Main background |
774
+ | `--chat-sidebar-bg` | Sidebar background |
775
+ | `--chat-border` | Border color |
776
+ | `--chat-text` | Primary text |
777
+ | `--chat-text-secondary` | Secondary text |
778
+ | `--chat-text-muted` | Muted text |
779
+ | `--chat-accent` | Accent color (buttons, links, badges, reply bars, pin icons) |
780
+ | `--chat-accent-transparent` | Accent with alpha (highlights, tints) |
781
+ | `--chat-accent-gradient` | Gradient for send button |
782
+ | `--chat-hover` | Hover background |
783
+ | `--chat-own-bubble-bg/border/text` | Own message bubble |
784
+ | `--chat-other-bubble-bg/border/text` | Other message bubble |
785
+ | `--chat-avatar-bg/text` | Avatar styling |
786
+ | `--chat-online-color/glow` | Online presence dot |
787
+ | `--chat-offline-color` | Offline presence dot |
788
+ | `--chat-unread-bg/text` | Unread badge |
789
+ | `--chat-input-bg/border/focus-border` | Input field |
790
+ | `--chat-send-bg/text` | Send button |
791
+ | `--chat-seen-color` | Read receipt checkmarks |
792
+ | `--chat-mention-bg/text` | @mention highlight |
793
+ | `--chat-attachment-bg` | Attachment card background |
794
+ | `--chat-attachment-btn-bg/hover` | Paperclip button |
795
+ | `--chat-attachment-active-bg/text` | Active attachment indicator |
796
+ | `--chat-link-color` | URL link color |
797
+ | `--chat-tab-bg/active-bg/active-color/active-border` | Tab bar |
798
+ | `--chat-modal-backdrop/bg/shadow` | Modal overlay (used by confirm dialog + new chat modal) |
799
+ | `--chat-error-color` | Error text |
800
+ | `--chat-transition-speed` | Animation speed |
801
+ | `--chat-radius` / `--chat-radius-sm` | Border radius |
802
+
803
+ Apps can reference their own variables: `'--chat-accent': 'var(--primary)'`.
804
+
805
+ ---
806
+
807
+ ## 20. Built-in Themes (30)
808
+
809
+ All themes exported from `themes.js` via `require('tacel-chat').themes`.
810
+
811
+ ### Light Themes (18)
812
+ | Name | Accent | Background |
813
+ |------|--------|------------|
814
+ | `default` | `#1976d2` | `#ffffff` |
815
+ | `light` | `#1976d2` | `#ffffff` |
816
+ | `ocean` | `#00897b` | `#f0fafa` |
817
+ | `forest` | `#2e7d32` | `#f2f7f2` |
818
+ | `sunset` | `#e65100` | `#fff8f0` |
819
+ | `rose` | `#c2185b` | `#fff5f8` |
820
+ | `lavender` | `#673ab7` | `#f8f5ff` |
821
+ | `slate` | `#495057` | `#f8f9fa` |
822
+ | `solarized-light` | `#268bd2` | `#fdf6e3` |
823
+ | `catppuccin-latte` | `#8839ef` | `#eff1f5` |
824
+ | `github-light` | `#0969da` | `#ffffff` |
825
+ | `office-hq` | `#c9a227` | `#ffffff` |
826
+ | `shipworks` | `#1976d2` | `#ffffff` |
827
+ | `coral` | `#ff5733` | `#fff5f3` |
828
+ | `mint` | `#00c853` | `#f0fff4` |
829
+ | `amber` | `#ffa000` | `#fffbf0` |
830
+ | `indigo` | `#303f9f` | `#f5f5ff` |
831
+ | `cream` | `#8b7750` | `#fefcf3` |
832
+
833
+ ### Dark Themes (12)
834
+ | Name | Accent | Background |
835
+ |------|--------|------------|
836
+ | `dark` | `#89b4fa` | `#1e1e2e` |
837
+ | `midnight` | `#6c7bd4` | `#0f0f1a` |
838
+ | `nord` | `#88c0d0` | `#2e3440` |
839
+ | `dracula` | `#bd93f9` | `#282a36` |
840
+ | `monokai` | `#a6e22e` | `#272822` |
841
+ | `solarized-dark` | `#268bd2` | `#002b36` |
842
+ | `catppuccin-mocha` | `#cba6f7` | `#1e1e2e` |
843
+ | `github-dark` | `#58a6ff` | `#0d1117` |
844
+ | `tech-portal` | `#53c1de` | `#1a1a2e` |
845
+ | `abyss` | `#4fc3f7` | `#060818` |
846
+ | `neon` | `#00ff88` | `#0a0a0a` |
847
+ | `cherry` | `#dc3c50` | `#1a0a0e` |
848
+
849
+ ---
850
+
851
+ ## 21. Backend API Factory
852
+
853
+ Optional factory that registers IPC handlers using app-provided DB functions:
854
+
855
+ ```js
856
+ const { initChatAPI } = require('tacel-chat/chat-api');
857
+ initChatAPI(ipcMain, {
858
+ channelPrefix: '',
859
+ dbQuery: (sql, params) => db.query('APP-CHATS', sql, params),
860
+ dbGetOne: (sql, params) => db.getOne('APP-CHATS', sql, params),
861
+ dbInsert: (table, data) => db.insert('APP-CHATS', table, data),
862
+ dbUpdate: (sql, cond, params) => db.update('APP-CHATS', sql, cond, params),
863
+ socketEmit: (event, payload) => socketClient.emit(event, payload),
864
+ broadcastRenderer: (eventName, payload) => { /* BrowserWindow broadcast */ },
865
+ });
866
+ ```
867
+
868
+ ---
869
+
870
+ ## 22. Real-Time Layer
871
+
872
+ Module does **not** own the transport. Apps connect their Socket.IO/polling to the instance's event methods:
873
+
874
+ ```js
875
+ chat.onNewMessage({ conversationId, messageId, sender, senderId, content, timestamp })
876
+ chat.onReadEvent({ conversationId, username, userId })
877
+ chat.onNewConversation({ conversationId })
878
+ chat.onPresenceUpdate({ userId, username, isOnline, lastSeen })
879
+ ```
880
+
881
+ 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.
882
+
883
+ ---
884
+
885
+ ## 23. Presence System
886
+
887
+ 1. On init: `onPresenceHeartbeat()` immediately
888
+ 2. Every `heartbeatMs`: `onPresenceHeartbeat()`
889
+ 3. On `beforeunload`: `onPresenceOffline()`
890
+ 4. Rendering: check `lastSeen` against `presenceStaleMs`
891
+
892
+ Apps without presence set `features.presence: false`.
893
+
894
+ ---
895
+
896
+ ## 24. Unread Counts & Read Receipts
897
+
898
+ **Unread counts**: derived from messages with no read receipt from current user. Fetched periodically and on socket events. Displayed as badges on conversations.
899
+
900
+ **Read receipts**: opening a conversation marks all unread as read. Only the last own message shows "checkmark Seen" with tooltip of who saw it.
901
+
902
+ Room mode does not use read receipts.
903
+
904
+ ---
905
+
906
+ ## 25. @Mentions & Notifications
907
+
908
+ Enabled via `features.mentions: true`.
909
+
910
+ **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`).
911
+
912
+ **Backend** (`features.mentionNotifications: true`): Extract `/@(\w+)/g`, resolve to user IDs, create notification rows, skip self-mentions. Room open > mark read.
913
+
914
+ ---
915
+
916
+ ## 26. User Sync / Registration
917
+
918
+ For conversation mode, users must be in `APP-CHATS`:
919
+
920
+ ```js
921
+ const { syncUser } = require('tacel-chat/chat-sync');
922
+ await syncUser({ dbQuery, dbGetOne, dbInsert, dbUpdate, original_id, app, username });
923
+ ```
924
+
925
+ Resolution: `global_key` first (cross-app unification) > `(original_id, app)` fallback > create if not found.
926
+
927
+ Room mode uses app's own `users` table -- no sync needed.
928
+
929
+ ---
930
+
931
+ ## 27. Public API
932
+
933
+ | Method | Description |
934
+ |--------|-------------|
935
+ | `new TacelChat()` | Create instance |
936
+ | `instance.initialize(container, config)` | Mount chat into DOM element |
937
+ | `instance.refresh()` | Re-fetch conversations/messages and re-render |
938
+ | `instance.openConversation(id)` | Programmatically open a conversation |
939
+ | `instance.switchRoom(roomType)` | Switch to a room tab (room mode) |
940
+ | `instance.onNewMessage(payload)` | Push real-time new message event |
941
+ | `instance.onReadEvent(payload)` | Push real-time read event |
942
+ | `instance.onNewConversation(payload)` | Push real-time new conversation event |
943
+ | `instance.onPresenceUpdate(payload)` | Push real-time presence event |
944
+ | `instance.pinMessage(msg)` | Programmatically pin/unpin a message |
945
+ | `instance.destroy()` | Clean up listeners, intervals, DOM |
946
+
947
+ ### Exported from `index.js`
948
+ ```js
949
+ const { TacelChat, initChatAPI, syncUser, normalizeUsername, themes, ConfirmDialog } = require('tacel-chat');
950
+ ```
951
+
952
+ ---
953
+
954
+ ## 28. IPC Channel Reference
955
+
956
+ ### Conversation Mode
957
+
958
+ | Channel | Params | Returns |
959
+ |---------|--------|---------|
960
+ | `chat-list-users` | -- | `[{ id, username, is_online, last_seen }]` |
961
+ | `chat-presence-heartbeat` | `{ current_username }` | `{ success }` |
962
+ | `chat-presence-offline` | `{ current_username }` | `{ success }` |
963
+ | `chat-get-conversations` | `{ current_username }` | `[{ id, type, display_name, last_message, last_timestamp }]` |
964
+ | `chat-get-messages` | `{ conversation_id }` | `[{ id, content, timestamp, sender, sender_id, reply_to, attachment }]` |
965
+ | `chat-start-direct` | `{ current_username, other_user_id }` | `{ success, conversation_id }` |
966
+ | `chat-send-message` | `{ conversation_id, current_username, content, attachment, reply_to }` | `{ success, message_id }` |
967
+ | `chat-get-unread-counts` | `{ current_username }` | `[{ conversation_id, unread_count }]` |
968
+ | `chat-mark-read` | `{ conversation_id, current_username }` | `{ success }` |
969
+ | `chat-get-message-reads` | `{ conversation_id }` | `[{ message_id, user_id, username, read_at }]` |
970
+
971
+ ### Attachment Channels
972
+
973
+ | Channel | Params | Returns |
974
+ |---------|--------|---------|
975
+ | `chat-upload-attachment` | `{ name, type, size, sourcePath }` | `{ success, path, name, type, size }` |
976
+ | `chat-open-file` | `{ path }` | `{ success }` |
977
+ | `chat-open-folder` | `{ path }` | `{ success }` |
978
+ | `chat-get-file-content` | `{ path }` | `{ success, data: { content } }` |
979
+
980
+ ### Pinned Message Channels
981
+
982
+ | Channel | Params | Returns |
983
+ |---------|--------|---------|
984
+ | `chat-pin-message` | `{ conversation_id, message_id }` | `{ success }` |
985
+ | `chat-unpin-message` | `{ conversation_id, message_id }` | `{ success }` |
986
+ | `chat-get-pinned-messages` | `{ conversation_id }` | `{ success, data: [{ id, content, senderName, timestamp }] }` |
987
+
988
+ ---
989
+
990
+ ## 29. Database Schema
991
+
992
+ ### Conversation Mode (`APP-CHATS` database)
993
+
994
+ ```sql
995
+ CREATE TABLE chat_users (
996
+ id INT AUTO_INCREMENT PRIMARY KEY,
997
+ original_id INT,
998
+ app VARCHAR(50),
999
+ username VARCHAR(100),
1000
+ global_key VARCHAR(100),
1001
+ is_online TINYINT DEFAULT 0,
1002
+ last_seen DATETIME
1003
+ );
1004
+
1005
+ CREATE TABLE chat_conversations (
1006
+ id INT AUTO_INCREMENT PRIMARY KEY,
1007
+ name VARCHAR(200),
1008
+ type ENUM('direct', 'team'),
1009
+ created_by INT,
1010
+ created_at DATETIME,
1011
+ updated_at DATETIME
1012
+ );
1013
+
1014
+ CREATE TABLE chat_participants (
1015
+ id INT AUTO_INCREMENT PRIMARY KEY,
1016
+ conversation_id INT,
1017
+ user_id INT,
1018
+ status ENUM('active', 'removed') DEFAULT 'active'
1019
+ );
1020
+
1021
+ CREATE TABLE chat_messages (
1022
+ id INT AUTO_INCREMENT PRIMARY KEY,
1023
+ conversation_id INT,
1024
+ sender_id INT,
1025
+ content TEXT,
1026
+ timestamp DATETIME,
1027
+ reply_to JSON,
1028
+ attachment JSON
1029
+ );
1030
+
1031
+ CREATE TABLE chat_message_reads (
1032
+ id INT AUTO_INCREMENT PRIMARY KEY,
1033
+ message_id INT,
1034
+ user_id INT,
1035
+ read_at DATETIME
1036
+ );
1037
+ ```
1038
+
1039
+ ---
1040
+
1041
+ ## 30. Test App
1042
+
1043
+ A full test application is available at `Random (rma,ticketing,more)/chat-test-app/`:
1044
+
1045
+ - **Electron app** with split-screen: left side (conversation mode) + right side (room mode)
1046
+ - **Two users** (Alice + Bob) with independent chat instances
1047
+ - **Theme selector** per side with all 30 themes
1048
+ - **Mock database** with seeded users, conversations, messages, and pinned message state
1049
+ - **Full attachment support**: file upload to local `chat-attachments/` folder, open file, open folder, inline image/PDF preview
1050
+ - **Reply support**: reply data stored and rendered
1051
+ - **Pin support**: pin/unpin messages, pinned panel with badge, click-to-scroll
1052
+ - **Search support**: in-chat keyword search with highlighted results
1053
+ - **Confirm dialog**: all alert/confirm popups use themed in-app modal
1054
+ - **All features enabled**: presence, read receipts, unread counts, search, new chat, attachments, mentions, pinned messages, search panel
1055
+
1056
+ ### Running the test app:
1057
+ ```bash
1058
+ cd "Random (rma,ticketing,more)/chat-test-app"
1059
+ npm install
1060
+ npx electron .
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ## 31. Integration Guide -- Conversation Mode
1066
+
1067
+ 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.
1068
+
1069
+ ### Architecture Overview
1070
+
1071
+ ```
1072
+ +----------------+ +----------------+ +----------------+
1073
+ | App A | | App B | | App C |
1074
+ | (renderer) | | (renderer) | | (renderer) |
1075
+ | tacel-chat | | tacel-chat | | tacel-chat |
1076
+ +-------+--------+ +-------+--------+ +-------+--------+
1077
+ | IPC | IPC | IPC
1078
+ +-------+--------+ +-------+--------+ +-------+--------+
1079
+ | App A | | App B | | App C |
1080
+ | (main) | | (main) | | (main) |
1081
+ | chat-api.js | | chat-api.js | | chat-api.js |
1082
+ | chat.js | | chat.js | | chat.js |
1083
+ +-------+--------+ +-------+--------+ +-------+--------+
1084
+ | | |
1085
+ v v v
1086
+ +-------------------------------------------------+
1087
+ | Shared MySQL Database |
1088
+ | (e.g. APP-CHATS) |
1089
+ | chat_users . chat_conversations . chat_messages |
1090
+ | chat_participants . chat_message_reads |
1091
+ +-------------------------------------------------+
1092
+ | | |
1093
+ v v v
1094
+ +-------------------------------------------------+
1095
+ | Socket.IO Relay Server |
1096
+ | One app instance hosts (port 3001) |
1097
+ | All others connect as clients |
1098
+ | Events: new_message, read, new_conversation |
1099
+ +-------------------------------------------------+
1100
+ ```
1101
+
1102
+ ### Cross-App User Identity
1103
+
1104
+ 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.
1105
+
1106
+ **How it works:**
1107
+
1108
+ 1. Each app has its own `users` table with its own user IDs
1109
+ 2. The shared chat database has a `chat_users` table with a `global_key` column
1110
+ 3. `global_key` = `username.trim().toLowerCase()` -- this is the cross-app identity key
1111
+ 4. When a user logs in, the app calls `syncUser({ original_id, app, username })`
1112
+ 5. Sync resolution order:
1113
+ - First: find by `global_key` (matches across all apps)
1114
+ - Fallback: find by `(original_id, app)` pair (legacy match within one app)
1115
+ - Not found: create a new `chat_users` row
1116
+
1117
+ **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.
1118
+
1119
+ ```js
1120
+ // In your app's login flow (main process):
1121
+ const { syncUser } = require('tacel-chat/chat-sync');
1122
+
1123
+ // After successful login, sync the user into the shared chat database
1124
+ const chatUser = await syncUser({
1125
+ original_id: appUser.id, // The user's ID in THIS app's database
1126
+ app: 'YourAppName', // Identifier for this app
1127
+ username: appUser.username, // The display username (used for global_key)
1128
+ // Provide your app's DB helpers:
1129
+ dbQuery, dbGetOne, dbInsert, dbUpdate
1130
+ });
1131
+ // chatUser = { id, original_id, app, username, global_key, is_online, last_seen }
1132
+ ```
1133
+
1134
+ ### Database Schema (Shared Chat Database)
1135
+
1136
+ The shared database (e.g. `APP-CHATS`) uses these tables:
1137
+
1138
+ ```sql
1139
+ -- Users from all apps, unified by global_key
1140
+ CREATE TABLE chat_users (
1141
+ id INT AUTO_INCREMENT PRIMARY KEY,
1142
+ original_id INT, -- User's ID in their source app
1143
+ app VARCHAR(50), -- Source app name (e.g. 'Office-HQ', 'ShipWorks')
1144
+ username VARCHAR(100), -- Display username
1145
+ global_key VARCHAR(100), -- Normalized: username.trim().toLowerCase()
1146
+ is_online TINYINT DEFAULT 0,
1147
+ last_seen DATETIME,
1148
+ last_active DATETIME
1149
+ );
1150
+
1151
+ -- Conversations (direct or team)
1152
+ CREATE TABLE chat_conversations (
1153
+ id INT AUTO_INCREMENT PRIMARY KEY,
1154
+ name VARCHAR(200), -- NULL for direct, team name for teams
1155
+ type ENUM('direct', 'team'),
1156
+ created_by INT, -- chat_users.id
1157
+ created_at DATETIME,
1158
+ updated_at DATETIME
1159
+ );
1160
+
1161
+ -- Who is in each conversation
1162
+ CREATE TABLE chat_participants (
1163
+ id INT AUTO_INCREMENT PRIMARY KEY,
1164
+ conversation_id INT,
1165
+ user_id INT, -- chat_users.id
1166
+ status ENUM('active', 'removed') DEFAULT 'active',
1167
+ joined_at DATETIME,
1168
+ removed_at DATETIME,
1169
+ removed_by INT
1170
+ );
1171
+
1172
+ -- Messages
1173
+ CREATE TABLE chat_messages (
1174
+ id INT AUTO_INCREMENT PRIMARY KEY,
1175
+ conversation_id INT,
1176
+ sender_id INT, -- chat_users.id
1177
+ content TEXT,
1178
+ timestamp DATETIME,
1179
+ reply_to JSON, -- { id, senderName, content } or NULL
1180
+ attachment JSON -- { name, type, size, path } or NULL
1181
+ );
1182
+
1183
+ -- Read receipts (per-message, per-user)
1184
+ CREATE TABLE chat_message_reads (
1185
+ id INT AUTO_INCREMENT PRIMARY KEY,
1186
+ message_id INT,
1187
+ user_id INT, -- chat_users.id
1188
+ read_at DATETIME
1189
+ );
1190
+ ```
1191
+
1192
+ ### Backend API Pattern
1193
+
1194
+ 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:
1195
+
1196
+ ```js
1197
+ const db = require('../db/dynamic-connection'); // Your app's DB helper
1198
+ const { emit: emitSocket, getSocket } = require('../main/socket-client');
1199
+ const { BrowserWindow } = require('electron');
1200
+
1201
+ const CHAT_DB = 'APP-CHATS'; // Your shared chat database name
1202
+
1203
+ function initChatAPI(ipcMain) {
1204
+ // Fallback: broadcast to all renderer windows when socket is down
1205
+ function broadcastRenderer(eventName, payload) {
1206
+ for (const w of BrowserWindow.getAllWindows()) {
1207
+ w.webContents.send(`socket:event:${eventName}`, payload);
1208
+ }
1209
+ }
1210
+
1211
+ ipcMain.handle('chat-list-users', async () => {
1212
+ return await db.query(CHAT_DB,
1213
+ 'SELECT id, username, app, is_online, last_seen, global_key FROM chat_users ORDER BY username ASC'
1214
+ );
1215
+ });
1216
+
1217
+ ipcMain.handle('chat-send-message', async (event, { conversation_id, current_username, content, attachment, reply_to }) => {
1218
+ const gk = current_username.trim().toLowerCase();
1219
+ const me = await db.getOne(CHAT_DB, 'SELECT id, username FROM chat_users WHERE global_key = ? LIMIT 1', [gk]);
1220
+ // ... insert message, touch conversation, insert sender read receipt ...
1221
+
1222
+ // Emit via Socket.IO for real-time delivery to all connected apps
1223
+ emitSocket('chat:new_message', { conversation_id, message_id, sender: me.username, content, timestamp });
1224
+
1225
+ // Fallback if socket is disconnected
1226
+ const s = getSocket();
1227
+ if (!s || !s.connected) {
1228
+ broadcastRenderer('chat:new_message', { conversation_id, message_id, sender: me.username, content, timestamp });
1229
+ }
1230
+
1231
+ return { success: true, message_id };
1232
+ });
1233
+
1234
+ // ... other handlers: chat-get-conversations, chat-get-messages, chat-start-direct,
1235
+ // chat-presence-heartbeat, chat-presence-offline, chat-get-unread-counts,
1236
+ // chat-mark-read, chat-get-message-reads, chat-get-direct-peers
1237
+ }
1238
+ ```
1239
+
1240
+ ### Socket.IO Real-Time Layer
1241
+
1242
+ The module does **not** include Socket.IO -- apps manage their own socket infrastructure.
1243
+
1244
+ **Socket Server** (one app instance hosts):
1245
+ - HTTP server + Socket.IO `Server` on a configurable port (default 3001)
1246
+ - Listens on `0.0.0.0` so all machines on the network can connect
1247
+ - Relays three events to all connected clients:
1248
+ - `chat:new_message` -- new message sent
1249
+ - `chat:read` -- conversation marked as read
1250
+ - `chat:new_conversation` -- new conversation created
1251
+
1252
+ **Socket Client** (all app instances connect):
1253
+ - Reads `socketServerUrl` from `version.json` (e.g. `http://192.168.x.x:3001`)
1254
+ - Socket.IO client with `autoConnect: true`
1255
+ - Exports `getSocket()` and `emit(event, payload)` with auto-reconnect queuing
1256
+
1257
+ **Configuration** (`version.json`):
1258
+ ```json
1259
+ {
1260
+ "socketServerUrl": "http://192.168.x.x:3001",
1261
+ "isSocketHost": false
1262
+ }
1263
+ ```
1264
+ One machine sets `isSocketHost: true` and starts the socket server. All others connect as clients.
1265
+
1266
+ ### Wiring Socket Events to the Module
1267
+
1268
+ In the renderer, listen for socket events and push them into the chat instance:
1269
+
1270
+ ```js
1271
+ // Listen for socket events forwarded from main process
1272
+ window.electronAPI.onSocketEvent('chat:new_message', (payload) => {
1273
+ chat.onNewMessage(payload);
1274
+ });
1275
+ window.electronAPI.onSocketEvent('chat:read', (payload) => {
1276
+ chat.onReadEvent(payload);
1277
+ });
1278
+ window.electronAPI.onSocketEvent('chat:new_conversation', (payload) => {
1279
+ chat.onNewConversation(payload);
1280
+ });
1281
+ ```
1282
+
1283
+ ### Presence System
1284
+
1285
+ 1. On login: `syncUser()` sets `is_online = 1`
1286
+ 2. Every 5s: `onPresenceHeartbeat()` > `UPDATE chat_users SET is_online = 1, last_seen = NOW() WHERE global_key = ?`
1287
+ 3. On `beforeunload`: `onPresenceOffline()` > `UPDATE chat_users SET is_online = 0, last_seen = NOW() WHERE global_key = ?`
1288
+ 4. Module checks `last_seen` against `presenceStaleMs` to determine online/offline status
1289
+
1290
+ ### Unread Counts & Read Receipts
1291
+
1292
+ - **Unread counts** are derived from messages with no read receipt from the current user:
1293
+ ```sql
1294
+ SELECT COUNT(*) FROM chat_messages m
1295
+ WHERE m.conversation_id = ? AND m.sender_id <> ?
1296
+ AND NOT EXISTS (SELECT 1 FROM chat_message_reads r WHERE r.message_id = m.id AND r.user_id = ?)
1297
+ ```
1298
+ - **Mark read**: insert `chat_message_reads` rows for all unread messages in the conversation
1299
+ - **Seen indicator**: only the last own message shows "checkmark Seen" with tooltip of who saw it
1300
+
1301
+ ---
1302
+
1303
+ ## 32. Integration Guide -- Room Mode
1304
+
1305
+ 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.
1306
+
1307
+ ### Architecture Overview
1308
+
1309
+ Room mode uses the **app's own database** (not the shared APP-CHATS database). Each app defines its own rooms, users, and messages tables.
1310
+
1311
+ ```
1312
+ +--------------------+
1313
+ | Wire-Scheduler |
1314
+ | (renderer) |
1315
+ | tacel-chat |
1316
+ | mode: 'room' |
1317
+ +---------+----------+
1318
+ | IPC
1319
+ +---------+----------+
1320
+ | Wire-Scheduler |
1321
+ | (main process) |
1322
+ | chat-api.js |
1323
+ | chat.js (DB) |
1324
+ +---------+----------+
1325
+ |
1326
+ +----+-----+
1327
+ | App DB | +--------------------+
1328
+ | (MySQL) | | Network Share |
1329
+ | rooms | | (attachments) |
1330
+ | messages | +--------------------+
1331
+ | users |
1332
+ +----------+
1333
+ ```
1334
+
1335
+ ### Database Schema (App's Own Database)
1336
+
1337
+ ```sql
1338
+ -- Predefined chat rooms
1339
+ CREATE TABLE chat_rooms (
1340
+ id INT AUTO_INCREMENT PRIMARY KEY,
1341
+ room_type VARCHAR(50), -- 'general', 'office', 'dev'
1342
+ name VARCHAR(100),
1343
+ description TEXT
1344
+ );
1345
+
1346
+ -- Messages in rooms
1347
+ CREATE TABLE chat_messages (
1348
+ id INT AUTO_INCREMENT PRIMARY KEY,
1349
+ room_id INT, -- chat_rooms.id
1350
+ user_id INT, -- users.id (app's own users table)
1351
+ message TEXT,
1352
+ created_at DATETIME,
1353
+ has_attachment TINYINT DEFAULT 0,
1354
+ attachment_path VARCHAR(500),
1355
+ attachment_type VARCHAR(100),
1356
+ attachment_name VARCHAR(255)
1357
+ );
1358
+
1359
+ -- App's own users table (not chat-specific)
1360
+ -- The app joins chat_messages.user_id to users.id for sender info
1361
+ ```
1362
+
1363
+ ### Room Mode Configuration
1364
+
1365
+ ```js
1366
+ chat.initialize(container, {
1367
+ mode: 'room',
1368
+ currentUsername: 'pierre',
1369
+ rooms: [
1370
+ { type: 'general', name: 'General', icon: 'fas fa-comments' },
1371
+ { type: 'office', name: 'Office', icon: 'fas fa-building' },
1372
+ { type: 'dev', name: 'Dev', icon: 'fas fa-code' },
1373
+ ],
1374
+ defaultRoom: 'general',
1375
+ messagePollingMs: 2000, // Poll for new messages every 2 seconds
1376
+ features: {
1377
+ attachments: true,
1378
+ attachmentPreview: true,
1379
+ mentions: true,
1380
+ mentionNotifications: true,
1381
+ tabs: true,
1382
+ urlLinkify: true,
1383
+ pinnedMessages: true,
1384
+ searchMessages: true,
1385
+ // No presence, read receipts, or unread counts in room mode
1386
+ presence: false,
1387
+ readReceipts: false,
1388
+ unreadCounts: false,
1389
+ },
1390
+ // Callbacks map to app's own IPC handlers
1391
+ onFetchMessages: async (roomType) => { /* fetch from app DB */ },
1392
+ onSendMessage: async (roomType, content, attachment, replyTo) => { /* insert into app DB */ },
1393
+ onUploadAttachment: async (attachment) => { /* save to network share */ },
1394
+ onFetchMentionUsers: async (conversationId) => { /* return app's users */ },
1395
+ onPinMessage: async (msg, conversationId) => { /* pin/unpin in app DB */ },
1396
+ onUnpinMessage: async (msg, conversationId) => { /* unpin in app DB */ },
1397
+ onFetchPinnedMessages: async (conversationId) => { /* return pinned messages */ },
1398
+ });
1399
+ ```
1400
+
1401
+ ### File Attachments on Network Share
1402
+
1403
+ Room-mode apps typically store attachments on a shared network drive. The app handles all file I/O -- the module only provides the UI:
1404
+
1405
+ ```js
1406
+ onUploadAttachment: async (attachment) => {
1407
+ // attachment = { name, type, size, path, file, data }
1408
+ // 'path' is set for file picker / drag-drop
1409
+ // 'data' (base64) is set for clipboard paste
1410
+ const result = await ipcInvoke('upload-chat-attachment', {
1411
+ name: attachment.name,
1412
+ type: attachment.type,
1413
+ sourcePath: attachment.path,
1414
+ data: attachment.data
1415
+ });
1416
+ return result; // { success, path, name, type, size }
1417
+ }
1418
+ ```
1419
+
1420
+ ### Polling vs Socket
1421
+
1422
+ 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.
1423
+
1424
+ ---
1425
+
1426
+ ## 33. Security & Public Module Guidelines
1427
+
1428
+ The chat module is designed to be **publicly distributable** with no sensitive information:
1429
+
1430
+ - **No credentials** -- no database passwords, connection strings, or API keys
1431
+ - **No network paths** -- no IP addresses, UNC paths, or server URLs
1432
+ - **No SQL** -- no raw queries, table names, or schema definitions in the module code
1433
+ - **No app-specific logic** -- the module is completely app-agnostic
1434
+ - **All sensitive config** is passed at runtime by the app via callbacks and config objects
1435
+ - **The module never connects to any network** -- apps handle all I/O through callbacks