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 +1435 -0
- package/chat-api.js +436 -0
- package/chat-sync.js +65 -0
- package/chat.css +2573 -0
- package/chat.js +834 -0
- package/components/attachment.js +400 -0
- package/components/confirm.js +125 -0
- package/components/context-menu.js +461 -0
- package/components/files-panel.js +228 -0
- package/components/mention.js +198 -0
- package/components/message-area.js +612 -0
- package/components/message.js +200 -0
- package/components/new-chat.js +130 -0
- package/components/pinned-panel.js +184 -0
- package/components/presence.js +45 -0
- package/components/search-panel.js +201 -0
- package/components/sidebar.js +278 -0
- package/components/tabs.js +130 -0
- package/index.js +12 -0
- package/package.json +41 -0
- package/themes.js +495 -0
- package/utils/dom.js +75 -0
- package/utils/format.js +133 -0
- package/utils/linkify.js +80 -0
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
|