clay-server 2.27.0-beta.11 → 2.27.0-beta.13

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.
@@ -0,0 +1,801 @@
1
+ // sidebar-mates.js - User/mate strip, DM picker, context menus, tooltips, presence
2
+ // Extracted from sidebar.js (PR-37)
3
+
4
+ import { userAvatarUrl, mateAvatarUrl } from './avatar.js';
5
+ import { escapeHtml } from './utils.js';
6
+ import { iconHtml, refreshIcons } from './icons.js';
7
+ import { showMateProfilePopover } from './profile.js';
8
+
9
+ var _ctx = null;
10
+
11
+ // --- User strip state ---
12
+ var cachedAllUsers = [];
13
+ var cachedOnlineUserIds = [];
14
+ var cachedDmFavorites = [];
15
+ var cachedDmConversations = [];
16
+ var cachedDmUnread = {};
17
+ var cachedMyUserId = null;
18
+ var currentDmUserId = null;
19
+ var dmPickerOpen = false;
20
+ var cachedDmRemovedUsers = {};
21
+ var cachedMates = [];
22
+
23
+ // --- Icon strip tooltip ---
24
+ var iconStripTooltip = null;
25
+
26
+ // --- DM user context menu ---
27
+ var userCtxMenu = null;
28
+
29
+ export function initSidebarMates(ctx) {
30
+ _ctx = ctx;
31
+ }
32
+
33
+ export function showIconTooltip(el, text) {
34
+ hideIconTooltip();
35
+ var tip = document.createElement("div");
36
+ tip.className = "icon-strip-tooltip";
37
+ tip.textContent = text;
38
+ document.body.appendChild(tip);
39
+ iconStripTooltip = tip;
40
+
41
+ requestAnimationFrame(function () {
42
+ var rect = el.getBoundingClientRect();
43
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
44
+ tip.classList.add("visible");
45
+ });
46
+ }
47
+
48
+ export function showIconTooltipHtml(el, html) {
49
+ hideIconTooltip();
50
+ var tip = document.createElement("div");
51
+ tip.className = "icon-strip-tooltip";
52
+ tip.style.whiteSpace = "normal";
53
+ tip.style.maxWidth = "260px";
54
+ tip.innerHTML = html;
55
+ document.body.appendChild(tip);
56
+ iconStripTooltip = tip;
57
+
58
+ requestAnimationFrame(function () {
59
+ var rect = el.getBoundingClientRect();
60
+ tip.style.top = (rect.top + rect.height / 2 - tip.offsetHeight / 2) + "px";
61
+ tip.classList.add("visible");
62
+ });
63
+ }
64
+
65
+ export function hideIconTooltip() {
66
+ if (iconStripTooltip) {
67
+ iconStripTooltip.remove();
68
+ iconStripTooltip = null;
69
+ }
70
+ }
71
+
72
+ export function closeUserCtxMenu() {
73
+ if (userCtxMenu) {
74
+ userCtxMenu.remove();
75
+ userCtxMenu = null;
76
+ }
77
+ document.removeEventListener("click", handleUserCtxOutsideClick, true);
78
+ }
79
+
80
+ function showUserCtxMenu(anchorEl, user) {
81
+ closeUserCtxMenu();
82
+ if (_ctx.closeProjectCtxMenu) _ctx.closeProjectCtxMenu();
83
+
84
+ var menu = document.createElement("div");
85
+ menu.className = "project-ctx-menu";
86
+
87
+ var removeItem = document.createElement("button");
88
+ removeItem.className = "project-ctx-item project-ctx-delete";
89
+ removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
90
+ removeItem.addEventListener("click", function (e) {
91
+ e.stopPropagation();
92
+ // Spawn dust particles at the user icon position
93
+ var iconRect = anchorEl.getBoundingClientRect();
94
+ if (_ctx.spawnDustParticles) _ctx.spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
95
+ closeUserCtxMenu();
96
+ // Immediately mark as removed so strip re-render hides the icon,
97
+ // even if the user was only visible via cachedDmConversations (not favorites)
98
+ cachedDmRemovedUsers[user.id] = true;
99
+ if (_ctx.onDmRemoveUser) _ctx.onDmRemoveUser(user.id);
100
+ renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers, cachedMates);
101
+ if (_ctx.sendWs) {
102
+ _ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
103
+ }
104
+ });
105
+ menu.appendChild(removeItem);
106
+
107
+ document.body.appendChild(menu);
108
+ userCtxMenu = menu;
109
+ refreshIcons();
110
+
111
+ requestAnimationFrame(function () {
112
+ var rect = anchorEl.getBoundingClientRect();
113
+ menu.style.position = "fixed";
114
+ menu.style.left = (rect.right + 6) + "px";
115
+ menu.style.top = rect.top + "px";
116
+ var menuRect = menu.getBoundingClientRect();
117
+ if (menuRect.right > window.innerWidth - 8) {
118
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
119
+ }
120
+ if (menuRect.bottom > window.innerHeight - 8) {
121
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
122
+ }
123
+ });
124
+
125
+ // Close on outside click
126
+ setTimeout(function () {
127
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
128
+ }, 0);
129
+ }
130
+
131
+ function handleUserCtxOutsideClick(e) {
132
+ if (userCtxMenu && !userCtxMenu.contains(e.target)) {
133
+ closeUserCtxMenu();
134
+ }
135
+ }
136
+
137
+ function showMateCtxMenu(anchorEl, mate) {
138
+ // Primary mates cannot be edited or removed
139
+ if (mate.primary) return;
140
+
141
+ closeUserCtxMenu();
142
+ if (_ctx.closeProjectCtxMenu) _ctx.closeProjectCtxMenu();
143
+
144
+ var menu = document.createElement("div");
145
+ menu.className = "project-ctx-menu";
146
+
147
+ // Edit Profile item
148
+ var editItem = document.createElement("button");
149
+ editItem.className = "project-ctx-item";
150
+ editItem.innerHTML = iconHtml("edit-2") + " <span>Edit Profile</span>";
151
+ editItem.addEventListener("click", function (e) {
152
+ e.stopPropagation();
153
+ closeUserCtxMenu();
154
+ showMateProfilePopover(anchorEl, mate, function (updates) {
155
+ if (_ctx.sendWs) {
156
+ _ctx.sendWs({ type: "mate_update", mateId: mate.id, updates: updates });
157
+ }
158
+ });
159
+ });
160
+ menu.appendChild(editItem);
161
+
162
+ var removeItem = document.createElement("button");
163
+ removeItem.className = "project-ctx-item";
164
+ removeItem.innerHTML = iconHtml("star-off") + " <span>Remove from favorites</span>";
165
+ removeItem.addEventListener("click", function (e) {
166
+ e.stopPropagation();
167
+ closeUserCtxMenu();
168
+ // Spawn dust particles at the mate icon position
169
+ var iconRect = anchorEl.getBoundingClientRect();
170
+ if (_ctx.spawnDustParticles) _ctx.spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
171
+ if (_ctx.sendWs) {
172
+ _ctx.sendWs({ type: "dm_remove_favorite", targetUserId: mate.id });
173
+ }
174
+ });
175
+ menu.appendChild(removeItem);
176
+
177
+ document.body.appendChild(menu);
178
+ userCtxMenu = menu;
179
+ refreshIcons();
180
+
181
+ requestAnimationFrame(function () {
182
+ var rect = anchorEl.getBoundingClientRect();
183
+ menu.style.position = "fixed";
184
+ menu.style.left = (rect.right + 6) + "px";
185
+ menu.style.top = rect.top + "px";
186
+ var menuRect = menu.getBoundingClientRect();
187
+ if (menuRect.right > window.innerWidth - 8) {
188
+ menu.style.left = (rect.left - menuRect.width - 6) + "px";
189
+ }
190
+ if (menuRect.bottom > window.innerHeight - 8) {
191
+ menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
192
+ }
193
+ });
194
+
195
+ setTimeout(function () {
196
+ document.addEventListener("click", handleUserCtxOutsideClick, true);
197
+ }, 0);
198
+ }
199
+
200
+ export function renderSidebarPresence(onlineUsers) {
201
+ var container = document.getElementById("sidebar-presence");
202
+ if (!container) return;
203
+ container.innerHTML = "";
204
+ if (!onlineUsers || onlineUsers.length < 2) return;
205
+ var maxShow = 4;
206
+ for (var i = 0; i < Math.min(onlineUsers.length, maxShow); i++) {
207
+ var ou = onlineUsers[i];
208
+ var img = document.createElement("img");
209
+ img.className = "sidebar-presence-avatar";
210
+ img.src = presenceAvatarUrl(ou);
211
+ img.alt = ou.displayName;
212
+ img.dataset.tip = ou.displayName + " (@" + ou.username + ")";
213
+ container.appendChild(img);
214
+ }
215
+ if (onlineUsers.length > maxShow) {
216
+ var more = document.createElement("span");
217
+ more.className = "sidebar-presence-more";
218
+ more.textContent = "+" + (onlineUsers.length - maxShow);
219
+ container.appendChild(more);
220
+ }
221
+ }
222
+
223
+ // Presence avatar URL helper
224
+ function presenceAvatarUrl(userOrStyle) {
225
+ if (userOrStyle && typeof userOrStyle === "object") return userAvatarUrl(userOrStyle, 24);
226
+ return userAvatarUrl({ avatarStyle: userOrStyle || "thumbs" }, 24);
227
+ }
228
+
229
+ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers, matesList) {
230
+ cachedMates = matesList || cachedMates || [];
231
+ cachedAllUsers = allUsers || [];
232
+ cachedOnlineUserIds = onlineUserIds || [];
233
+ cachedDmFavorites = dmFavorites || [];
234
+ cachedDmConversations = dmConversations || [];
235
+ cachedDmUnread = dmUnread || {};
236
+ cachedDmRemovedUsers = dmRemovedUsers || {};
237
+ cachedMyUserId = myUserId;
238
+ var container = document.getElementById("icon-strip-users");
239
+ if (!container) return;
240
+
241
+ // All other users
242
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
243
+
244
+ // Hide section if no other users and no mates
245
+ if (allOthers.length === 0 && cachedMates.length === 0) {
246
+ container.innerHTML = "";
247
+ container.classList.add("hidden");
248
+ return;
249
+ }
250
+
251
+ // Filter to show only: favorites + users with unread + users with DM conversations
252
+ // But exclude users explicitly removed from favorites
253
+ var others = allOthers.filter(function (u) {
254
+ if (cachedDmRemovedUsers[u.id]) return false;
255
+ if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
256
+ if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
257
+ if (cachedDmConversations.indexOf(u.id) !== -1) return true;
258
+ return false;
259
+ });
260
+
261
+ container.classList.remove("hidden");
262
+ container.innerHTML = "";
263
+
264
+ for (var i = 0; i < others.length; i++) {
265
+ (function (u) {
266
+ var el = document.createElement("div");
267
+ el.className = "icon-strip-user";
268
+ el.dataset.userId = u.id;
269
+ if (u.id === currentDmUserId) el.classList.add("active");
270
+ if (onlineUserIds.indexOf(u.id) !== -1) el.classList.add("online");
271
+
272
+ var pill = document.createElement("span");
273
+ pill.className = "icon-strip-pill";
274
+ el.appendChild(pill);
275
+
276
+ var avatar = document.createElement("img");
277
+ avatar.className = "icon-strip-user-avatar";
278
+ avatar.src = userAvatarUrl(u, 34);
279
+ avatar.alt = u.displayName;
280
+ el.appendChild(avatar);
281
+
282
+ var onlineDot = document.createElement("span");
283
+ onlineDot.className = "icon-strip-user-online";
284
+ el.appendChild(onlineDot);
285
+
286
+ var badge = document.createElement("span");
287
+ badge.className = "icon-strip-user-badge";
288
+ badge.dataset.userId = u.id;
289
+ el.appendChild(badge);
290
+
291
+ // Tooltip
292
+ el.addEventListener("mouseenter", function () { showIconTooltip(el, u.displayName); });
293
+ el.addEventListener("mouseleave", hideIconTooltip);
294
+
295
+ // Click: open DM
296
+ el.addEventListener("click", function () {
297
+ if (_ctx.openDm) _ctx.openDm(u.id);
298
+ });
299
+
300
+ // Right-click: show context menu
301
+ el.addEventListener("contextmenu", function (e) {
302
+ e.preventDefault();
303
+ e.stopPropagation();
304
+ showUserCtxMenu(el, u);
305
+ });
306
+
307
+ container.appendChild(el);
308
+ })(others[i]);
309
+ }
310
+
311
+ // Build mate project status lookup from project list
312
+ var mateProjectStatus = {};
313
+ if (_ctx && _ctx.projectList) {
314
+ var allProjects = _ctx.projectList;
315
+ for (var pi = 0; pi < allProjects.length; pi++) {
316
+ if (allProjects[pi].isMate) {
317
+ mateProjectStatus[allProjects[pi].slug] = allProjects[pi];
318
+ }
319
+ }
320
+ }
321
+
322
+ // Render mates (only favorites, built-in first, then user-created)
323
+ var favoriteMates = cachedMates.filter(function (m) {
324
+ if (cachedDmRemovedUsers[m.id]) return false;
325
+ if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
326
+ if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
327
+ return false;
328
+ });
329
+ var sortedMates = favoriteMates.sort(function (a, b) {
330
+ var aBuiltin = a.builtinKey ? 1 : 0;
331
+ var bBuiltin = b.builtinKey ? 1 : 0;
332
+ if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
333
+ return (a.createdAt || 0) - (b.createdAt || 0);
334
+ });
335
+ for (var mi = 0; mi < sortedMates.length; mi++) {
336
+ (function (mate) {
337
+ var mp = mate.profile || {};
338
+ var mateSlug = "mate-" + mate.id;
339
+ var mateProj = mateProjectStatus[mateSlug] || {};
340
+ var isActive = mate.id === currentDmUserId;
341
+ var el = document.createElement("div");
342
+ el.className = "icon-strip-user icon-strip-mate";
343
+ el.dataset.userId = mate.id;
344
+ el.dataset.mateSlug = mateSlug;
345
+ if (isActive) el.classList.add("active");
346
+
347
+ // Pending permission shake
348
+ if (mateProj.pendingPermissions > 0 && !isActive) {
349
+ el.classList.add("has-pending-perm");
350
+ }
351
+
352
+ var pill = document.createElement("span");
353
+ pill.className = "icon-strip-pill";
354
+ el.appendChild(pill);
355
+
356
+ var avatar = document.createElement("img");
357
+ avatar.className = "icon-strip-user-avatar" + (mate.primary ? " icon-strip-primary-mate" : "");
358
+ avatar.src = mateAvatarUrl(mate, 34);
359
+ avatar.alt = mp.displayName || mate.name || "Mate";
360
+ var mateColor = (mp.avatarColor) || mate.avatarColor || "#7c3aed";
361
+ avatar.style.background = mateColor + "30";
362
+ el.appendChild(avatar);
363
+
364
+ // Processing status dot (IO blink)
365
+ var statusDot = document.createElement("span");
366
+ statusDot.className = "icon-strip-status";
367
+ if (mateProj.isProcessing) statusDot.classList.add("processing");
368
+ el.appendChild(statusDot);
369
+
370
+ // Mate badge (bot icon)
371
+ var mateBadge = document.createElement("span");
372
+ mateBadge.className = "icon-strip-user-mate-badge";
373
+ mateBadge.innerHTML = iconHtml("bot");
374
+ el.appendChild(mateBadge);
375
+
376
+ var badge = document.createElement("span");
377
+ badge.className = "icon-strip-user-badge";
378
+ badge.dataset.userId = mate.id;
379
+ el.appendChild(badge);
380
+
381
+ // Restore unread badge if cached
382
+ var unreadCount = cachedDmUnread[mate.id] || 0;
383
+ if (unreadCount > 0 && !isActive) {
384
+ badge.textContent = unreadCount > 99 ? "99+" : String(unreadCount);
385
+ badge.classList.add("has-unread");
386
+ }
387
+
388
+ // Tooltip
389
+ var displayName = mp.displayName || mate.name || "New Mate";
390
+ el.addEventListener("mouseenter", function () {
391
+ var html = '<div style="font-weight:600">' + escapeHtml(displayName);
392
+ if (mate.primary) {
393
+ html += ' <span style="font-size:10px;font-weight:600;color:#00b894;background:rgba(0,184,148,0.1);padding:1px 5px;border-radius:3px;margin-left:4px">SYSTEM</span>';
394
+ }
395
+ html += '</div>';
396
+ if (mate.bio) {
397
+ html += '<div style="font-weight:400;font-size:12px;color:var(--text-secondary);margin-top:2px">' + escapeHtml(mate.bio) + '</div>';
398
+ }
399
+ showIconTooltipHtml(el, html);
400
+ });
401
+ el.addEventListener("mouseleave", hideIconTooltip);
402
+
403
+ // Click: open DM with mate
404
+ el.addEventListener("click", function () {
405
+ if (_ctx.openDm) _ctx.openDm(mate.id);
406
+ });
407
+
408
+ // Right-click: context menu for mate
409
+ el.addEventListener("contextmenu", function (e) {
410
+ e.preventDefault();
411
+ e.stopPropagation();
412
+ showMateCtxMenu(el, mate);
413
+ });
414
+
415
+ container.appendChild(el);
416
+ })(sortedMates[mi]);
417
+ }
418
+
419
+ // Show container if we have mates even with no other users
420
+ if (cachedMates.length > 0) {
421
+ container.classList.remove("hidden");
422
+ }
423
+
424
+ // Add user (+) button
425
+ var addBtn = document.createElement("button");
426
+ addBtn.className = "icon-strip-invite";
427
+ addBtn.innerHTML = iconHtml("user-plus");
428
+ addBtn.addEventListener("click", function (e) {
429
+ e.stopPropagation();
430
+ toggleDmUserPicker(addBtn);
431
+ });
432
+ addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add user or create mate"); });
433
+ addBtn.addEventListener("mouseleave", hideIconTooltip);
434
+ container.appendChild(addBtn);
435
+ refreshIcons();
436
+ }
437
+
438
+ function toggleDmUserPicker(anchorEl) {
439
+ if (dmPickerOpen) {
440
+ closeDmUserPicker();
441
+ return;
442
+ }
443
+ dmPickerOpen = true;
444
+
445
+ var picker = document.createElement("div");
446
+ picker.className = "dm-user-picker";
447
+ picker.id = "dm-user-picker";
448
+
449
+ // Search input
450
+ var searchInput = document.createElement("input");
451
+ searchInput.className = "dm-user-picker-search";
452
+ searchInput.type = "text";
453
+ searchInput.placeholder = "Search mates and users...";
454
+ picker.appendChild(searchInput);
455
+
456
+ // User list element (appended later, after USERS label)
457
+ var listEl = document.createElement("div");
458
+ listEl.className = "dm-user-picker-list";
459
+
460
+ // Position the picker above the + button
461
+ document.body.appendChild(picker);
462
+ var rect = anchorEl.getBoundingClientRect();
463
+ picker.style.left = (rect.right + 8) + "px";
464
+ picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
465
+
466
+ function renderPickerList(filter) {
467
+ listEl.innerHTML = "";
468
+ var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
469
+ // Exclude already-favorited users
470
+ var available = allOthers.filter(function (u) {
471
+ return cachedDmFavorites.indexOf(u.id) === -1;
472
+ });
473
+ if (filter) {
474
+ var lf = filter.toLowerCase();
475
+ available = available.filter(function (u) {
476
+ return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
477
+ (u.username && u.username.toLowerCase().indexOf(lf) !== -1);
478
+ });
479
+ }
480
+ if (available.length === 0) {
481
+ var emptyEl = document.createElement("div");
482
+ emptyEl.className = "dm-user-picker-empty";
483
+ emptyEl.textContent = filter ? "No users found" : "No more users to add";
484
+ listEl.appendChild(emptyEl);
485
+ return;
486
+ }
487
+ for (var i = 0; i < available.length; i++) {
488
+ (function (u) {
489
+ var item = document.createElement("div");
490
+ item.className = "dm-user-picker-item";
491
+
492
+ var av = document.createElement("img");
493
+ av.className = "dm-user-picker-avatar";
494
+ av.src = userAvatarUrl(u, 28);
495
+ av.alt = u.displayName;
496
+ item.appendChild(av);
497
+
498
+ var name = document.createElement("span");
499
+ name.className = "dm-user-picker-name";
500
+ name.textContent = u.displayName;
501
+ item.appendChild(name);
502
+
503
+ item.addEventListener("click", function () {
504
+ if (_ctx.sendWs) {
505
+ _ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
506
+ }
507
+ closeDmUserPicker();
508
+ });
509
+
510
+ listEl.appendChild(item);
511
+ })(available[i]);
512
+ }
513
+ }
514
+
515
+ // --- MATES section ---
516
+ var matesSectionLabel = document.createElement("div");
517
+ matesSectionLabel.className = "dm-user-picker-section";
518
+ matesSectionLabel.textContent = "Mates";
519
+ picker.appendChild(matesSectionLabel);
520
+
521
+ var matesListEl = document.createElement("div");
522
+ matesListEl.className = "dm-user-picker-list dm-mates-list";
523
+ picker.appendChild(matesListEl);
524
+
525
+ // Update scroll gradient hint
526
+ function updateMatesScrollHint() {
527
+ var isOverflow = matesListEl.scrollHeight > matesListEl.clientHeight + 2;
528
+ if (!isOverflow) {
529
+ matesListEl.classList.add("no-overflow");
530
+ matesListEl.classList.remove("scrolled-bottom");
531
+ return;
532
+ }
533
+ matesListEl.classList.remove("no-overflow");
534
+ var atBottom = matesListEl.scrollTop + matesListEl.clientHeight >= matesListEl.scrollHeight - 4;
535
+ if (atBottom) {
536
+ matesListEl.classList.add("scrolled-bottom");
537
+ } else {
538
+ matesListEl.classList.remove("scrolled-bottom");
539
+ }
540
+ }
541
+ matesListEl.addEventListener("scroll", updateMatesScrollHint);
542
+
543
+ function renderMatesList(filter) {
544
+ matesListEl.innerHTML = "";
545
+ var allMates = cachedMates || [];
546
+ if (filter) {
547
+ var lf = filter.toLowerCase();
548
+ allMates = allMates.filter(function (m) {
549
+ var name = (m.profile && m.profile.displayName) || m.name || "";
550
+ return name.toLowerCase().indexOf(lf) !== -1;
551
+ });
552
+ }
553
+ // Build unified list: installed builtins, deleted builtins, user-created
554
+ var availBuiltins = (_ctx.availableBuiltins && _ctx.availableBuiltins()) || [];
555
+ var entries = [];
556
+ // 1. Installed builtin mates
557
+ for (var si = 0; si < allMates.length; si++) {
558
+ if (allMates[si].builtinKey) entries.push({ type: "mate", data: allMates[si] });
559
+ }
560
+ // 2. Deleted builtins (only when not filtering)
561
+ if (!filter) {
562
+ for (var di = 0; di < availBuiltins.length; di++) {
563
+ entries.push({ type: "deleted", data: availBuiltins[di] });
564
+ }
565
+ }
566
+ // 3. User-created mates
567
+ var userMates = allMates.filter(function (m) { return !m.builtinKey; });
568
+ userMates.sort(function (a, b) { return (a.createdAt || 0) - (b.createdAt || 0); });
569
+ for (var ui = 0; ui < userMates.length; ui++) {
570
+ entries.push({ type: "mate", data: userMates[ui] });
571
+ }
572
+
573
+ for (var i = 0; i < entries.length; i++) {
574
+ var entry = entries[i];
575
+ if (entry.type === "deleted") {
576
+ // Deleted builtin: show with "+ Add" button
577
+ (function (b) {
578
+ var bItem = document.createElement("div");
579
+ bItem.className = "dm-user-picker-item dm-user-picker-builtin-item";
580
+ bItem.style.opacity = "0.7";
581
+ var bAv = document.createElement("img");
582
+ bAv.className = "dm-user-picker-avatar";
583
+ bAv.src = mateAvatarUrl({ avatarCustom: b.avatarCustom, avatarStyle: b.avatarStyle || "bottts", avatarSeed: b.displayName, id: b.key }, 28);
584
+ bAv.alt = b.displayName;
585
+ bItem.appendChild(bAv);
586
+ var bNameWrap = document.createElement("div");
587
+ bNameWrap.style.cssText = "flex:1;min-width:0;";
588
+ var bName = document.createElement("span");
589
+ bName.className = "dm-user-picker-name";
590
+ bName.textContent = b.displayName;
591
+ bNameWrap.appendChild(bName);
592
+ var bBio = document.createElement("div");
593
+ bBio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
594
+ bBio.textContent = b.bio || b.displayName;
595
+ bNameWrap.appendChild(bBio);
596
+ bItem.appendChild(bNameWrap);
597
+ var bAddBtn = document.createElement("button");
598
+ bAddBtn.style.cssText = "border:none;background:none;cursor:pointer;padding:2px 6px;color:var(--accent, #6366f1);font-size:12px;font-weight:600;white-space:nowrap;";
599
+ bAddBtn.textContent = "+ Add";
600
+ bAddBtn.title = "Re-add " + b.displayName;
601
+ bAddBtn.addEventListener("click", function (e) {
602
+ e.stopPropagation();
603
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
604
+ closeDmUserPicker();
605
+ });
606
+ bItem.appendChild(bAddBtn);
607
+ bItem.addEventListener("click", function () {
608
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_readd_builtin", builtinKey: b.key });
609
+ closeDmUserPicker();
610
+ });
611
+ matesListEl.appendChild(bItem);
612
+ })(entry.data);
613
+ } else {
614
+ // Normal mate
615
+ (function (m) {
616
+ var mp = m.profile || {};
617
+ var isFav = cachedDmFavorites.indexOf(m.id) !== -1;
618
+ var item = document.createElement("div");
619
+ item.className = "dm-user-picker-item";
620
+ if (isFav) item.classList.add("dm-picker-fav");
621
+ var av = document.createElement("img");
622
+ av.className = "dm-user-picker-avatar";
623
+ av.src = mateAvatarUrl(m, 28);
624
+ av.alt = mp.displayName || m.name || "Mate";
625
+ item.appendChild(av);
626
+ var nameWrap = document.createElement("div");
627
+ nameWrap.style.cssText = "flex:1;min-width:0;";
628
+ var name = document.createElement("span");
629
+ name.className = "dm-user-picker-name";
630
+ name.textContent = mp.displayName || m.name || "Mate";
631
+ nameWrap.appendChild(name);
632
+ if (m.bio) {
633
+ var bio = document.createElement("div");
634
+ bio.style.cssText = "font-size:11px;color:var(--text-dimmer);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
635
+ bio.textContent = m.bio;
636
+ nameWrap.appendChild(bio);
637
+ }
638
+ item.appendChild(nameWrap);
639
+ // Delete button with inline confirm
640
+ var delBtn = document.createElement("button");
641
+ delBtn.className = "dm-picker-del-btn";
642
+ delBtn.innerHTML = m.builtinKey ? iconHtml("minus-circle") : iconHtml("trash-2");
643
+ delBtn.title = m.builtinKey ? "Remove mate" : "Delete mate";
644
+ delBtn.addEventListener("click", function (e) {
645
+ e.stopPropagation();
646
+ var origHtml = item.innerHTML;
647
+ item.innerHTML = "";
648
+ item.style.justifyContent = "center";
649
+ item.style.gap = "6px";
650
+ var confirmMsg = document.createElement("span");
651
+ confirmMsg.style.cssText = "font-size:12px;color:var(--text-dimmer);";
652
+ confirmMsg.textContent = m.builtinKey ? "Remove? You can add back anytime." : "Delete permanently?";
653
+ item.appendChild(confirmMsg);
654
+ var yesBtn = document.createElement("button");
655
+ yesBtn.style.cssText = "border:none;background:var(--danger,#e74c3c);color:#fff;padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
656
+ yesBtn.textContent = m.builtinKey ? "Remove" : "Delete";
657
+ yesBtn.addEventListener("click", function (e2) {
658
+ e2.stopPropagation();
659
+ if (_ctx.sendWs) _ctx.sendWs({ type: "mate_delete", mateId: m.id });
660
+ closeDmUserPicker();
661
+ });
662
+ item.appendChild(yesBtn);
663
+ var noBtn = document.createElement("button");
664
+ noBtn.style.cssText = "border:1px solid var(--border);background:none;color:var(--text);padding:3px 10px;border-radius:4px;font-size:12px;cursor:pointer;";
665
+ noBtn.textContent = "Cancel";
666
+ noBtn.addEventListener("click", function (e2) {
667
+ e2.stopPropagation();
668
+ item.innerHTML = origHtml;
669
+ item.style.justifyContent = "";
670
+ item.style.gap = "";
671
+ refreshIcons();
672
+ });
673
+ item.appendChild(noBtn);
674
+ });
675
+ item.appendChild(delBtn);
676
+ item.addEventListener("click", function () {
677
+ if (_ctx.openDm) _ctx.openDm(m.id);
678
+ if (!isFav && _ctx.sendWs) _ctx.sendWs({ type: "dm_add_favorite", targetUserId: m.id });
679
+ closeDmUserPicker();
680
+ });
681
+ matesListEl.appendChild(item);
682
+ })(entry.data);
683
+ }
684
+ }
685
+
686
+ if (entries.length === 0 && filter) {
687
+ var emptyEl = document.createElement("div");
688
+ emptyEl.className = "dm-user-picker-empty";
689
+ emptyEl.textContent = "No mates found";
690
+ matesListEl.appendChild(emptyEl);
691
+ }
692
+ refreshIcons();
693
+ requestAnimationFrame(updateMatesScrollHint);
694
+ }
695
+
696
+ // Create Mate option
697
+ var createMateEl = document.createElement("div");
698
+ createMateEl.className = "dm-user-picker-create-mate";
699
+ var hasCustomMates = (cachedMates || []).some(function (m) { return !m.builtinKey; });
700
+ var createMateLabel = hasCustomMates ? "Create a Mate" : "Create a Mate for what you're doing";
701
+ createMateEl.innerHTML = iconHtml("bot") + " <span>" + createMateLabel + "</span>";
702
+ createMateEl.addEventListener("click", function () {
703
+ closeDmUserPicker();
704
+ if (_ctx.openMateWizard) _ctx.openMateWizard();
705
+ });
706
+ picker.appendChild(createMateEl);
707
+
708
+ // Divider
709
+ var divider = document.createElement("div");
710
+ divider.style.borderTop = "1px solid var(--border, #333)";
711
+ divider.style.margin = "4px 0";
712
+ picker.appendChild(divider);
713
+
714
+ // Section label for users
715
+ var sectionLabel = document.createElement("div");
716
+ sectionLabel.className = "dm-user-picker-section";
717
+ sectionLabel.textContent = "Users";
718
+ picker.appendChild(sectionLabel);
719
+ picker.appendChild(listEl);
720
+
721
+ renderMatesList("");
722
+ renderPickerList("");
723
+ searchInput.addEventListener("input", function () {
724
+ var val = searchInput.value;
725
+ renderMatesList(val);
726
+ renderPickerList(val);
727
+ });
728
+
729
+ // Focus search
730
+ setTimeout(function () { searchInput.focus(); }, 50);
731
+
732
+ // Close on click outside
733
+ function onDocClick(e) {
734
+ if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
735
+ closeDmUserPicker();
736
+ document.removeEventListener("click", onDocClick, true);
737
+ }
738
+ }
739
+ setTimeout(function () {
740
+ document.addEventListener("click", onDocClick, true);
741
+ }, 10);
742
+ picker._docClickHandler = onDocClick;
743
+ }
744
+
745
+ export function closeDmUserPicker() {
746
+ dmPickerOpen = false;
747
+ var picker = document.getElementById("dm-user-picker");
748
+ if (picker) {
749
+ if (picker._docClickHandler) {
750
+ document.removeEventListener("click", picker._docClickHandler, true);
751
+ }
752
+ picker.remove();
753
+ }
754
+ }
755
+
756
+ export function setCurrentDmUser(userId) {
757
+ currentDmUserId = userId;
758
+ // Update active state on user icons immediately
759
+ var container = document.getElementById("icon-strip-users");
760
+ if (!container) return;
761
+ var items = container.querySelectorAll(".icon-strip-user");
762
+ for (var i = 0; i < items.length; i++) {
763
+ if (items[i].dataset.userId === userId) {
764
+ items[i].classList.add("active");
765
+ } else {
766
+ items[i].classList.remove("active");
767
+ }
768
+ }
769
+ }
770
+
771
+ export function updateDmBadge(userId, count) {
772
+ var badge = document.querySelector('.icon-strip-user-badge[data-user-id="' + userId + '"]');
773
+ if (!badge) return;
774
+ if (count > 0) {
775
+ badge.textContent = count > 99 ? "99+" : String(count);
776
+ badge.classList.add("has-unread");
777
+ } else {
778
+ badge.textContent = "";
779
+ badge.classList.remove("has-unread");
780
+ }
781
+ }
782
+
783
+ export function getCurrentDmUserId() {
784
+ return currentDmUserId;
785
+ }
786
+
787
+ export function getCachedMates() {
788
+ return cachedMates;
789
+ }
790
+
791
+ export function getCachedDmFavorites() {
792
+ return cachedDmFavorites;
793
+ }
794
+
795
+ export function getCachedDmUnread() {
796
+ return cachedDmUnread;
797
+ }
798
+
799
+ export function getCachedDmRemovedUsers() {
800
+ return cachedDmRemovedUsers;
801
+ }