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/chat-api.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tacel-chat — Backend IPC handler factory
|
|
3
|
+
* Registers all chat IPC handlers using app-provided database functions.
|
|
4
|
+
* This is optional — apps can implement their own handlers instead.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function normalizeUsername(username) {
|
|
8
|
+
return (username || '').trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize chat IPC handlers
|
|
13
|
+
* @param {object} ipcMain - Electron ipcMain
|
|
14
|
+
* @param {object} options
|
|
15
|
+
* @param {string} options.channelPrefix - Optional prefix for IPC channels
|
|
16
|
+
* @param {Function} options.dbQuery - (sql, params) => Promise<Array>
|
|
17
|
+
* @param {Function} options.dbGetOne - (sql, params) => Promise<object|null>
|
|
18
|
+
* @param {Function} options.socketEmit - (event, payload) => void
|
|
19
|
+
* @param {Function} options.broadcastRenderer - (eventName, payload) => void (fallback)
|
|
20
|
+
*/
|
|
21
|
+
function initChatAPI(ipcMain, options = {}) {
|
|
22
|
+
const prefix = options.channelPrefix || '';
|
|
23
|
+
const dbQuery = options.dbQuery;
|
|
24
|
+
const dbGetOne = options.dbGetOne;
|
|
25
|
+
const socketEmit = options.socketEmit || (() => {});
|
|
26
|
+
const broadcastRenderer = options.broadcastRenderer || (() => {});
|
|
27
|
+
|
|
28
|
+
function ch(name) { return prefix + name; }
|
|
29
|
+
|
|
30
|
+
function emit(event, payload) {
|
|
31
|
+
try {
|
|
32
|
+
socketEmit(event, payload);
|
|
33
|
+
} catch {
|
|
34
|
+
broadcastRenderer(event, payload);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── List Users ──────────────────────────────────────────────
|
|
39
|
+
ipcMain.handle(ch('chat-list-users'), async () => {
|
|
40
|
+
try {
|
|
41
|
+
const rows = await dbQuery(
|
|
42
|
+
'SELECT id, username, app, is_online, last_seen, global_key FROM chat_users ORDER BY username'
|
|
43
|
+
);
|
|
44
|
+
return { success: true, data: rows };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error('[TacelChatAPI] chat-list-users error:', err);
|
|
47
|
+
return { success: false, error: err.message };
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ── Presence Heartbeat ──────────────────────────────────────
|
|
52
|
+
ipcMain.handle(ch('chat-presence-heartbeat'), async (event, { current_username }) => {
|
|
53
|
+
try {
|
|
54
|
+
const gk = normalizeUsername(current_username);
|
|
55
|
+
await dbQuery(
|
|
56
|
+
'UPDATE chat_users SET is_online = 1, last_seen = CURRENT_TIMESTAMP WHERE global_key = ?',
|
|
57
|
+
[gk]
|
|
58
|
+
);
|
|
59
|
+
return { success: true };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('[TacelChatAPI] chat-presence-heartbeat error:', err);
|
|
62
|
+
return { success: false, error: err.message };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── Presence Offline ────────────────────────────────────────
|
|
67
|
+
ipcMain.handle(ch('chat-presence-offline'), async (event, { current_username }) => {
|
|
68
|
+
try {
|
|
69
|
+
const gk = normalizeUsername(current_username);
|
|
70
|
+
await dbQuery(
|
|
71
|
+
'UPDATE chat_users SET is_online = 0, last_seen = CURRENT_TIMESTAMP WHERE global_key = ?',
|
|
72
|
+
[gk]
|
|
73
|
+
);
|
|
74
|
+
return { success: true };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('[TacelChatAPI] chat-presence-offline error:', err);
|
|
77
|
+
return { success: false, error: err.message };
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── Get Direct Peers ────────────────────────────────────────
|
|
82
|
+
ipcMain.handle(ch('chat-get-direct-peers'), async (event, { current_username }) => {
|
|
83
|
+
try {
|
|
84
|
+
const gk = normalizeUsername(current_username);
|
|
85
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
86
|
+
if (!me) return { success: true, data: [] };
|
|
87
|
+
|
|
88
|
+
const rows = await dbQuery(`
|
|
89
|
+
SELECT DISTINCT p2.user_id
|
|
90
|
+
FROM chat_participants p1
|
|
91
|
+
JOIN chat_participants p2 ON p1.conversation_id = p2.conversation_id
|
|
92
|
+
JOIN chat_conversations c ON c.id = p1.conversation_id
|
|
93
|
+
WHERE p1.user_id = ? AND p2.user_id != ? AND c.type = 'direct'
|
|
94
|
+
AND p1.status = 'active' AND p2.status = 'active'
|
|
95
|
+
`, [me.id, me.id]);
|
|
96
|
+
|
|
97
|
+
return { success: true, data: rows.map(r => r.user_id) };
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.error('[TacelChatAPI] chat-get-direct-peers error:', err);
|
|
100
|
+
return { success: false, error: err.message };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── Get Conversations ───────────────────────────────────────
|
|
105
|
+
ipcMain.handle(ch('chat-get-conversations'), async (event, { current_username }) => {
|
|
106
|
+
try {
|
|
107
|
+
const gk = normalizeUsername(current_username);
|
|
108
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
109
|
+
if (!me) return { success: true, data: [] };
|
|
110
|
+
|
|
111
|
+
const rows = await dbQuery(`
|
|
112
|
+
SELECT c.id, c.type, c.name,
|
|
113
|
+
CASE
|
|
114
|
+
WHEN c.type = 'direct' THEN (
|
|
115
|
+
SELECT u.username FROM chat_participants p2
|
|
116
|
+
JOIN chat_users u ON u.id = p2.user_id
|
|
117
|
+
WHERE p2.conversation_id = c.id AND p2.user_id != ? AND p2.status = 'active'
|
|
118
|
+
LIMIT 1
|
|
119
|
+
)
|
|
120
|
+
ELSE c.name
|
|
121
|
+
END AS display_name,
|
|
122
|
+
(SELECT m.content FROM chat_messages m WHERE m.conversation_id = c.id ORDER BY m.timestamp DESC LIMIT 1) AS last_message,
|
|
123
|
+
(SELECT m.timestamp FROM chat_messages m WHERE m.conversation_id = c.id ORDER BY m.timestamp DESC LIMIT 1) AS last_timestamp
|
|
124
|
+
FROM chat_conversations c
|
|
125
|
+
JOIN chat_participants p ON p.conversation_id = c.id
|
|
126
|
+
WHERE p.user_id = ? AND p.status = 'active'
|
|
127
|
+
ORDER BY last_timestamp DESC
|
|
128
|
+
`, [me.id, me.id]);
|
|
129
|
+
|
|
130
|
+
return { success: true, data: rows };
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[TacelChatAPI] chat-get-conversations error:', err);
|
|
133
|
+
return { success: false, error: err.message };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── Get Messages ────────────────────────────────────────────
|
|
138
|
+
ipcMain.handle(ch('chat-get-messages'), async (event, { conversation_id }) => {
|
|
139
|
+
try {
|
|
140
|
+
const rows = await dbQuery(`
|
|
141
|
+
SELECT m.id, m.content, m.timestamp, u.username AS sender, m.sender_id
|
|
142
|
+
FROM chat_messages m
|
|
143
|
+
JOIN chat_users u ON u.id = m.sender_id
|
|
144
|
+
WHERE m.conversation_id = ?
|
|
145
|
+
ORDER BY m.timestamp ASC
|
|
146
|
+
`, [conversation_id]);
|
|
147
|
+
return { success: true, data: rows };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error('[TacelChatAPI] chat-get-messages error:', err);
|
|
150
|
+
return { success: false, error: err.message };
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Start Direct Conversation ───────────────────────────────
|
|
155
|
+
ipcMain.handle(ch('chat-start-direct'), async (event, { current_username, other_user_id }) => {
|
|
156
|
+
try {
|
|
157
|
+
const gk = normalizeUsername(current_username);
|
|
158
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
159
|
+
if (!me) return { success: false, error: 'User not found' };
|
|
160
|
+
|
|
161
|
+
// Check for existing direct conversation
|
|
162
|
+
const existing = await dbGetOne(`
|
|
163
|
+
SELECT p1.conversation_id
|
|
164
|
+
FROM chat_participants p1
|
|
165
|
+
JOIN chat_participants p2 ON p1.conversation_id = p2.conversation_id
|
|
166
|
+
JOIN chat_conversations c ON c.id = p1.conversation_id
|
|
167
|
+
WHERE p1.user_id = ? AND p2.user_id = ? AND c.type = 'direct'
|
|
168
|
+
AND p1.status = 'active' AND p2.status = 'active'
|
|
169
|
+
`, [me.id, other_user_id]);
|
|
170
|
+
|
|
171
|
+
if (existing) {
|
|
172
|
+
return { success: true, conversation_id: existing.conversation_id };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Create new conversation
|
|
176
|
+
const convResult = await dbQuery(
|
|
177
|
+
"INSERT INTO chat_conversations (type, created_by, created_at, updated_at) VALUES ('direct', ?, NOW(), NOW())",
|
|
178
|
+
[me.id]
|
|
179
|
+
);
|
|
180
|
+
const convId = convResult.insertId;
|
|
181
|
+
|
|
182
|
+
// Add participants
|
|
183
|
+
await dbQuery(
|
|
184
|
+
'INSERT INTO chat_participants (conversation_id, user_id, status, joined_at) VALUES (?, ?, \'active\', NOW())',
|
|
185
|
+
[convId, me.id]
|
|
186
|
+
);
|
|
187
|
+
await dbQuery(
|
|
188
|
+
'INSERT INTO chat_participants (conversation_id, user_id, status, joined_at) VALUES (?, ?, \'active\', NOW())',
|
|
189
|
+
[convId, other_user_id]
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
emit('chat:new_conversation', { conversation_id: convId });
|
|
193
|
+
return { success: true, conversation_id: convId };
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('[TacelChatAPI] chat-start-direct error:', err);
|
|
196
|
+
return { success: false, error: err.message };
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ── Send Message ────────────────────────────────────────────
|
|
201
|
+
ipcMain.handle(ch('chat-send-message'), async (event, { conversation_id, current_username, content }) => {
|
|
202
|
+
try {
|
|
203
|
+
const gk = normalizeUsername(current_username);
|
|
204
|
+
const me = await dbGetOne('SELECT id, username FROM chat_users WHERE global_key = ?', [gk]);
|
|
205
|
+
if (!me) return { success: false, error: 'User not found' };
|
|
206
|
+
|
|
207
|
+
const result = await dbQuery(
|
|
208
|
+
'INSERT INTO chat_messages (conversation_id, sender_id, content, timestamp) VALUES (?, ?, ?, NOW())',
|
|
209
|
+
[conversation_id, me.id, content]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Touch conversation updated_at
|
|
213
|
+
await dbQuery('UPDATE chat_conversations SET updated_at = NOW() WHERE id = ?', [conversation_id]);
|
|
214
|
+
|
|
215
|
+
// Insert sender's own read receipt
|
|
216
|
+
await dbQuery(
|
|
217
|
+
'INSERT INTO chat_message_reads (message_id, user_id, read_at) VALUES (?, ?, NOW())',
|
|
218
|
+
[result.insertId, me.id]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
emit('chat:new_message', {
|
|
222
|
+
conversation_id,
|
|
223
|
+
message_id: result.insertId,
|
|
224
|
+
sender: me.username,
|
|
225
|
+
sender_id: me.id,
|
|
226
|
+
content,
|
|
227
|
+
timestamp: new Date().toISOString()
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return { success: true, message_id: result.insertId };
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('[TacelChatAPI] chat-send-message error:', err);
|
|
233
|
+
return { success: false, error: err.message };
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ── Get Unread Counts ───────────────────────────────────────
|
|
238
|
+
ipcMain.handle(ch('chat-get-unread-counts'), async (event, { current_username }) => {
|
|
239
|
+
try {
|
|
240
|
+
const gk = normalizeUsername(current_username);
|
|
241
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
242
|
+
if (!me) return { success: true, data: [] };
|
|
243
|
+
|
|
244
|
+
const rows = await dbQuery(`
|
|
245
|
+
SELECT m.conversation_id, COUNT(*) AS unread_count
|
|
246
|
+
FROM chat_messages m
|
|
247
|
+
JOIN chat_participants p ON p.conversation_id = m.conversation_id AND p.user_id = ? AND p.status = 'active'
|
|
248
|
+
WHERE m.sender_id != ?
|
|
249
|
+
AND NOT EXISTS (
|
|
250
|
+
SELECT 1 FROM chat_message_reads r WHERE r.message_id = m.id AND r.user_id = ?
|
|
251
|
+
)
|
|
252
|
+
GROUP BY m.conversation_id
|
|
253
|
+
`, [me.id, me.id, me.id]);
|
|
254
|
+
|
|
255
|
+
return { success: true, data: rows };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error('[TacelChatAPI] chat-get-unread-counts error:', err);
|
|
258
|
+
return { success: false, error: err.message };
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ── Mark Read ───────────────────────────────────────────────
|
|
263
|
+
ipcMain.handle(ch('chat-mark-read'), async (event, { conversation_id, current_username }) => {
|
|
264
|
+
try {
|
|
265
|
+
const gk = normalizeUsername(current_username);
|
|
266
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
267
|
+
if (!me) return { success: true, marked: 0 };
|
|
268
|
+
|
|
269
|
+
// Find unread messages from others
|
|
270
|
+
const unread = await dbQuery(`
|
|
271
|
+
SELECT m.id FROM chat_messages m
|
|
272
|
+
WHERE m.conversation_id = ? AND m.sender_id != ?
|
|
273
|
+
AND NOT EXISTS (
|
|
274
|
+
SELECT 1 FROM chat_message_reads r WHERE r.message_id = m.id AND r.user_id = ?
|
|
275
|
+
)
|
|
276
|
+
`, [conversation_id, me.id, me.id]);
|
|
277
|
+
|
|
278
|
+
for (const msg of unread) {
|
|
279
|
+
await dbQuery(
|
|
280
|
+
'INSERT INTO chat_message_reads (message_id, user_id, read_at) VALUES (?, ?, NOW())',
|
|
281
|
+
[msg.id, me.id]
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (unread.length > 0) {
|
|
286
|
+
emit('chat:read', { conversation_id, user_id: me.id, username: current_username });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { success: true, marked: unread.length };
|
|
290
|
+
} catch (err) {
|
|
291
|
+
console.error('[TacelChatAPI] chat-mark-read error:', err);
|
|
292
|
+
return { success: false, error: err.message };
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ── Get Message Reads ───────────────────────────────────────
|
|
297
|
+
ipcMain.handle(ch('chat-get-message-reads'), async (event, { conversation_id }) => {
|
|
298
|
+
try {
|
|
299
|
+
const rows = await dbQuery(`
|
|
300
|
+
SELECT r.message_id, r.user_id, u.username, r.read_at
|
|
301
|
+
FROM chat_message_reads r
|
|
302
|
+
JOIN chat_users u ON u.id = r.user_id
|
|
303
|
+
JOIN chat_messages m ON m.id = r.message_id
|
|
304
|
+
WHERE m.conversation_id = ?
|
|
305
|
+
ORDER BY r.read_at ASC
|
|
306
|
+
`, [conversation_id]);
|
|
307
|
+
return { success: true, data: rows };
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error('[TacelChatAPI] chat-get-message-reads error:', err);
|
|
310
|
+
return { success: false, error: err.message };
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// ── Create Team ─────────────────────────────────────────────
|
|
315
|
+
ipcMain.handle(ch('chat-create-team'), async (event, { current_username, name, member_ids }) => {
|
|
316
|
+
try {
|
|
317
|
+
const gk = normalizeUsername(current_username);
|
|
318
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
319
|
+
if (!me) return { success: false, error: 'User not found' };
|
|
320
|
+
if (!member_ids || member_ids.length < 2) return { success: false, error: 'Need at least 2 members' };
|
|
321
|
+
|
|
322
|
+
// Ensure creator is in member list
|
|
323
|
+
const allMembers = new Set(member_ids.map(Number));
|
|
324
|
+
allMembers.add(me.id);
|
|
325
|
+
|
|
326
|
+
const convResult = await dbQuery(
|
|
327
|
+
"INSERT INTO chat_conversations (type, name, created_by, created_at, updated_at) VALUES ('team', ?, ?, NOW(), NOW())",
|
|
328
|
+
[name, me.id]
|
|
329
|
+
);
|
|
330
|
+
const convId = convResult.insertId;
|
|
331
|
+
|
|
332
|
+
for (const uid of allMembers) {
|
|
333
|
+
await dbQuery(
|
|
334
|
+
'INSERT INTO chat_participants (conversation_id, user_id, status, joined_at) VALUES (?, ?, \'active\', NOW())',
|
|
335
|
+
[convId, uid]
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
emit('chat:new_conversation', { conversation_id: convId });
|
|
340
|
+
return { success: true, conversation_id: convId };
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('[TacelChatAPI] chat-create-team error:', err);
|
|
343
|
+
return { success: false, error: err.message };
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ── Update Team ─────────────────────────────────────────────
|
|
348
|
+
ipcMain.handle(ch('chat-update-team'), async (event, { conversation_id, name, member_ids, current_username }) => {
|
|
349
|
+
try {
|
|
350
|
+
if (name) {
|
|
351
|
+
await dbQuery('UPDATE chat_conversations SET name = ?, updated_at = NOW() WHERE id = ?', [name, conversation_id]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (member_ids && member_ids.length > 0) {
|
|
355
|
+
const gk = normalizeUsername(current_username);
|
|
356
|
+
const me = await dbGetOne('SELECT id FROM chat_users WHERE global_key = ?', [gk]);
|
|
357
|
+
|
|
358
|
+
// Get current active members
|
|
359
|
+
const current = await dbQuery(
|
|
360
|
+
"SELECT user_id FROM chat_participants WHERE conversation_id = ? AND status = 'active'",
|
|
361
|
+
[conversation_id]
|
|
362
|
+
);
|
|
363
|
+
const currentIds = new Set(current.map(r => r.user_id));
|
|
364
|
+
const newIds = new Set(member_ids.map(Number));
|
|
365
|
+
|
|
366
|
+
// Remove members not in new list (except self)
|
|
367
|
+
for (const uid of currentIds) {
|
|
368
|
+
if (!newIds.has(uid) && me && uid !== me.id) {
|
|
369
|
+
await dbQuery(
|
|
370
|
+
"UPDATE chat_participants SET status = 'removed', removed_at = NOW(), removed_by = ? WHERE conversation_id = ? AND user_id = ?",
|
|
371
|
+
[me ? me.id : null, conversation_id, uid]
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Add new members
|
|
377
|
+
for (const uid of newIds) {
|
|
378
|
+
if (!currentIds.has(uid)) {
|
|
379
|
+
await dbQuery(
|
|
380
|
+
'INSERT INTO chat_participants (conversation_id, user_id, status, joined_at) VALUES (?, ?, \'active\', NOW())',
|
|
381
|
+
[conversation_id, uid]
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { success: true };
|
|
388
|
+
} catch (err) {
|
|
389
|
+
console.error('[TacelChatAPI] chat-update-team error:', err);
|
|
390
|
+
return { success: false, error: err.message };
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ── Delete Team ─────────────────────────────────────────────
|
|
395
|
+
ipcMain.handle(ch('chat-delete-team'), async (event, { conversation_id }) => {
|
|
396
|
+
try {
|
|
397
|
+
// Delete reads for messages in this conversation
|
|
398
|
+
await dbQuery(`
|
|
399
|
+
DELETE r FROM chat_message_reads r
|
|
400
|
+
JOIN chat_messages m ON m.id = r.message_id
|
|
401
|
+
WHERE m.conversation_id = ?
|
|
402
|
+
`, [conversation_id]);
|
|
403
|
+
await dbQuery('DELETE FROM chat_messages WHERE conversation_id = ?', [conversation_id]);
|
|
404
|
+
await dbQuery('DELETE FROM chat_participants WHERE conversation_id = ?', [conversation_id]);
|
|
405
|
+
await dbQuery('DELETE FROM chat_conversations WHERE id = ?', [conversation_id]);
|
|
406
|
+
return { success: true };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.error('[TacelChatAPI] chat-delete-team error:', err);
|
|
409
|
+
return { success: false, error: err.message };
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ── List Teams ──────────────────────────────────────────────
|
|
414
|
+
ipcMain.handle(ch('chat-list-teams'), async () => {
|
|
415
|
+
try {
|
|
416
|
+
const rows = await dbQuery(`
|
|
417
|
+
SELECT c.id, c.name,
|
|
418
|
+
GROUP_CONCAT(p.user_id) AS member_ids,
|
|
419
|
+
GROUP_CONCAT(u.username) AS member_usernames
|
|
420
|
+
FROM chat_conversations c
|
|
421
|
+
JOIN chat_participants p ON p.conversation_id = c.id AND p.status = 'active'
|
|
422
|
+
JOIN chat_users u ON u.id = p.user_id
|
|
423
|
+
WHERE c.type = 'team'
|
|
424
|
+
GROUP BY c.id, c.name
|
|
425
|
+
`);
|
|
426
|
+
return { success: true, data: rows };
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error('[TacelChatAPI] chat-list-teams error:', err);
|
|
429
|
+
return { success: false, error: err.message };
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
console.log(`[TacelChatAPI] Registered chat IPC handlers${prefix ? ` with prefix "${prefix}"` : ''}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
module.exports = { initChatAPI };
|
package/chat-sync.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tacel-chat — User sync utility
|
|
3
|
+
* Registers/updates a user in the shared APP-CHATS database.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function normalizeUsername(username) {
|
|
7
|
+
return (username || '').trim().toLowerCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sync a user into the chat_users table
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {Function} options.dbQuery - (sql, params) => Promise<Array>
|
|
14
|
+
* @param {Function} options.dbGetOne - (sql, params) => Promise<object|null>
|
|
15
|
+
* @param {number|string} options.original_id - User's ID in the app's own DB
|
|
16
|
+
* @param {string} options.app - App identifier (e.g., 'Office-HQ')
|
|
17
|
+
* @param {string} options.username - Display name
|
|
18
|
+
* @returns {Promise<object>} The synced user record
|
|
19
|
+
*/
|
|
20
|
+
async function syncUser({ dbQuery, dbGetOne, original_id, app, username }) {
|
|
21
|
+
if (!original_id || !app || !username) {
|
|
22
|
+
throw new Error('syncUser requires original_id, app, username');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const global_key = normalizeUsername(username);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Try to find by global_key first (cross-app unification)
|
|
29
|
+
let existing = await dbGetOne('SELECT * FROM chat_users WHERE global_key = ?', [global_key]);
|
|
30
|
+
|
|
31
|
+
// Fallback to (original_id, app) pair
|
|
32
|
+
if (!existing) {
|
|
33
|
+
existing = await dbGetOne(
|
|
34
|
+
'SELECT * FROM chat_users WHERE original_id = ? AND app = ?',
|
|
35
|
+
[original_id, app]
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (existing) {
|
|
40
|
+
// Update existing user
|
|
41
|
+
await dbQuery(
|
|
42
|
+
'UPDATE chat_users SET username = ?, global_key = ?, app = ?, original_id = ?, is_online = 1, last_seen = CURRENT_TIMESTAMP, last_active = CURRENT_TIMESTAMP WHERE id = ?',
|
|
43
|
+
[username, global_key, app, original_id, existing.id]
|
|
44
|
+
);
|
|
45
|
+
const updated = await dbGetOne('SELECT * FROM chat_users WHERE id = ?', [existing.id]);
|
|
46
|
+
console.log(`[ChatSync] Updated existing chat user id=${existing.id}`);
|
|
47
|
+
return updated || { ...existing, username, global_key, app, original_id };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create new user
|
|
51
|
+
const result = await dbQuery(
|
|
52
|
+
'INSERT INTO chat_users (original_id, app, username, global_key, is_online, last_seen, last_active) VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
|
|
53
|
+
[original_id, app, username, global_key]
|
|
54
|
+
);
|
|
55
|
+
const id = result.insertId;
|
|
56
|
+
const created = await dbGetOne('SELECT * FROM chat_users WHERE id = ?', [id]);
|
|
57
|
+
console.log(`[ChatSync] Created new chat user id=${id}`);
|
|
58
|
+
return created || { id, original_id, app, username, global_key };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('[ChatSync] Failed to sync user:', { original_id, app, username, error: err.message });
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { syncUser, normalizeUsername };
|