clay-server 2.11.0-beta.8 → 2.11.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/bin/cli.js +282 -115
- package/lib/daemon.js +109 -37
- package/lib/os-users.js +58 -1
- package/lib/pages.js +31 -29
- package/lib/project.js +47 -9
- package/lib/public/app.js +158 -16
- package/lib/public/css/filebrowser.css +6 -0
- package/lib/public/css/icon-strip.css +123 -1
- package/lib/public/css/messages.css +1 -1
- package/lib/public/css/mobile-nav.css +17 -0
- package/lib/public/css/overlays.css +49 -0
- package/lib/public/css/sidebar.css +26 -0
- package/lib/public/css/sticky-notes.css +3 -0
- package/lib/public/index.html +2 -0
- package/lib/public/modules/admin.js +53 -5
- package/lib/public/modules/sidebar.js +299 -21
- package/lib/public/modules/sticky-notes.js +27 -5
- package/lib/public/modules/terminal.js +161 -25
- package/lib/sdk-bridge.js +53 -7
- package/lib/server.js +156 -17
- package/lib/sessions.js +48 -7
- package/lib/terminal-manager.js +4 -2
- package/lib/terminal.js +2 -1
- package/lib/users.js +92 -0
- package/package.json +1 -1
|
@@ -24,6 +24,27 @@ function showConfirmDialog(message, onConfirm) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function showConfirmResetPin(name, onConfirm) {
|
|
28
|
+
var modal = document.createElement("div");
|
|
29
|
+
modal.className = "admin-modal-overlay";
|
|
30
|
+
var html = '<div class="admin-modal">' +
|
|
31
|
+
'<div class="admin-modal-body" style="padding:20px 16px 16px">' +
|
|
32
|
+
'<p class="admin-modal-desc" style="margin:0;font-size:14px;color:var(--text)">Reset PIN for <strong>' + escapeHtml(name) + '</strong>? A new temporary PIN will be generated and they will need to change it on next login.</p>' +
|
|
33
|
+
'</div>' +
|
|
34
|
+
'<div class="admin-modal-footer">' +
|
|
35
|
+
'<button class="admin-modal-save">Reset PIN</button>' +
|
|
36
|
+
'<button class="admin-modal-cancel">Cancel</button>' +
|
|
37
|
+
'</div></div>';
|
|
38
|
+
modal.innerHTML = html;
|
|
39
|
+
document.body.appendChild(modal);
|
|
40
|
+
modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
|
|
41
|
+
modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
|
|
42
|
+
modal.querySelector(".admin-modal-save").addEventListener("click", function () {
|
|
43
|
+
modal.remove();
|
|
44
|
+
onConfirm();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
var ctx = null;
|
|
28
49
|
var cachedUsers = [];
|
|
29
50
|
var cachedInvites = [];
|
|
@@ -111,8 +132,13 @@ function renderUsersTab(body) {
|
|
|
111
132
|
html += '</div>';
|
|
112
133
|
html += '<div class="admin-user-meta">' + escapeHtml(u.username) + ' · joined ' + created + '</div>';
|
|
113
134
|
html += '</div>';
|
|
114
|
-
if (!isMe
|
|
115
|
-
html += '<
|
|
135
|
+
if (!isMe) {
|
|
136
|
+
html += '<div style="display:flex;align-items:center;gap:2px">';
|
|
137
|
+
html += '<button class="admin-remove-btn admin-reset-pin-btn" data-user-id="' + u.id + '" title="Reset PIN">' + iconHtml("key-round") + '</button>';
|
|
138
|
+
if (u.role !== "admin") {
|
|
139
|
+
html += '<button class="admin-remove-btn" data-user-id="' + u.id + '" title="Remove user">' + iconHtml("trash-2") + '</button>';
|
|
140
|
+
}
|
|
141
|
+
html += '</div>';
|
|
116
142
|
}
|
|
117
143
|
html += '</div>';
|
|
118
144
|
}
|
|
@@ -129,10 +155,32 @@ function renderUsersTab(body) {
|
|
|
129
155
|
});
|
|
130
156
|
}
|
|
131
157
|
|
|
158
|
+
// Bind reset PIN buttons
|
|
159
|
+
var resetBtns = body.querySelectorAll(".admin-reset-pin-btn");
|
|
160
|
+
for (var j = 0; j < resetBtns.length; j++) {
|
|
161
|
+
resetBtns[j].addEventListener("click", function (e) {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
var userId = this.dataset.userId;
|
|
164
|
+
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
165
|
+
var name = user ? (user.displayName || user.username) : "this user";
|
|
166
|
+
showConfirmResetPin(name, function () {
|
|
167
|
+
apiPost("/api/admin/users/" + userId + "/reset-pin").then(function (data) {
|
|
168
|
+
if (data.ok) {
|
|
169
|
+
showTempPinModal({ username: user.username, displayName: user.displayName || user.username }, data.tempPin);
|
|
170
|
+
} else {
|
|
171
|
+
showToast(data.error || "Failed to reset PIN");
|
|
172
|
+
}
|
|
173
|
+
}).catch(function () {
|
|
174
|
+
showToast("Failed to reset PIN");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
// Bind remove buttons
|
|
133
|
-
var removeBtns = body.querySelectorAll(".admin-remove-btn");
|
|
134
|
-
for (var
|
|
135
|
-
removeBtns[
|
|
181
|
+
var removeBtns = body.querySelectorAll(".admin-remove-btn:not(.admin-reset-pin-btn)");
|
|
182
|
+
for (var k = 0; k < removeBtns.length; k++) {
|
|
183
|
+
removeBtns[k].addEventListener("click", function () {
|
|
136
184
|
var userId = this.dataset.userId;
|
|
137
185
|
var user = cachedUsers.find(function (u) { return u.id === userId; });
|
|
138
186
|
var name = user ? (user.displayName || user.username) : "this user";
|
|
@@ -435,6 +435,16 @@ function renderSessionItem(s) {
|
|
|
435
435
|
})(s.id, s.title, s.cliSessionId, moreBtn, s));
|
|
436
436
|
el.appendChild(moreBtn);
|
|
437
437
|
|
|
438
|
+
// Unread badge
|
|
439
|
+
var unreadBadge = document.createElement("span");
|
|
440
|
+
unreadBadge.className = "session-unread-badge";
|
|
441
|
+
unreadBadge.dataset.sessionId = s.id;
|
|
442
|
+
if (s.unread > 0) {
|
|
443
|
+
unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
|
|
444
|
+
unreadBadge.classList.add("has-unread");
|
|
445
|
+
}
|
|
446
|
+
el.appendChild(unreadBadge);
|
|
447
|
+
|
|
438
448
|
el.addEventListener("click", (function (id) {
|
|
439
449
|
return function () {
|
|
440
450
|
if (ctx.ws && ctx.connected) {
|
|
@@ -708,6 +718,13 @@ function renderSheetProjects(listEl) {
|
|
|
708
718
|
el.appendChild(dot);
|
|
709
719
|
}
|
|
710
720
|
|
|
721
|
+
if (p.unread > 0 && p.slug !== cachedCurrentSlug) {
|
|
722
|
+
var mBadge = document.createElement("span");
|
|
723
|
+
mBadge.className = "mobile-project-unread";
|
|
724
|
+
mBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
725
|
+
el.appendChild(mBadge);
|
|
726
|
+
}
|
|
727
|
+
|
|
711
728
|
el.addEventListener("click", function () {
|
|
712
729
|
if (ctx.switchProject) ctx.switchProject(p.slug);
|
|
713
730
|
closeMobileSheet();
|
|
@@ -1387,6 +1404,74 @@ function hideIconTooltip() {
|
|
|
1387
1404
|
}
|
|
1388
1405
|
}
|
|
1389
1406
|
|
|
1407
|
+
// --- DM user context menu ---
|
|
1408
|
+
var userCtxMenu = null;
|
|
1409
|
+
|
|
1410
|
+
function closeUserCtxMenu() {
|
|
1411
|
+
if (userCtxMenu) {
|
|
1412
|
+
userCtxMenu.remove();
|
|
1413
|
+
userCtxMenu = null;
|
|
1414
|
+
}
|
|
1415
|
+
document.removeEventListener("click", handleUserCtxOutsideClick, true);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function showUserCtxMenu(anchorEl, user) {
|
|
1419
|
+
closeUserCtxMenu();
|
|
1420
|
+
closeProjectCtxMenu();
|
|
1421
|
+
|
|
1422
|
+
var menu = document.createElement("div");
|
|
1423
|
+
menu.className = "project-ctx-menu";
|
|
1424
|
+
|
|
1425
|
+
var removeItem = document.createElement("button");
|
|
1426
|
+
removeItem.className = "project-ctx-item project-ctx-delete";
|
|
1427
|
+
removeItem.innerHTML = iconHtml("user-minus") + " <span>Remove from favorites</span>";
|
|
1428
|
+
removeItem.addEventListener("click", function (e) {
|
|
1429
|
+
e.stopPropagation();
|
|
1430
|
+
// Spawn dust particles at the user icon position
|
|
1431
|
+
var iconRect = anchorEl.getBoundingClientRect();
|
|
1432
|
+
spawnDustParticles(iconRect.left + iconRect.width / 2, iconRect.top + iconRect.height / 2);
|
|
1433
|
+
closeUserCtxMenu();
|
|
1434
|
+
// Immediately mark as removed so strip re-render hides the icon,
|
|
1435
|
+
// even if the user was only visible via cachedDmConversations (not favorites)
|
|
1436
|
+
cachedDmRemovedUsers[user.id] = true;
|
|
1437
|
+
if (ctx.onDmRemoveUser) ctx.onDmRemoveUser(user.id);
|
|
1438
|
+
renderUserStrip(cachedAllUsers, cachedOnlineUserIds, cachedMyUserId, cachedDmFavorites, cachedDmConversations, cachedDmUnread, cachedDmRemovedUsers);
|
|
1439
|
+
if (ctx.sendWs) {
|
|
1440
|
+
ctx.sendWs({ type: "dm_remove_favorite", targetUserId: user.id });
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
menu.appendChild(removeItem);
|
|
1444
|
+
|
|
1445
|
+
document.body.appendChild(menu);
|
|
1446
|
+
userCtxMenu = menu;
|
|
1447
|
+
refreshIcons();
|
|
1448
|
+
|
|
1449
|
+
requestAnimationFrame(function () {
|
|
1450
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
1451
|
+
menu.style.position = "fixed";
|
|
1452
|
+
menu.style.left = (rect.right + 6) + "px";
|
|
1453
|
+
menu.style.top = rect.top + "px";
|
|
1454
|
+
var menuRect = menu.getBoundingClientRect();
|
|
1455
|
+
if (menuRect.right > window.innerWidth - 8) {
|
|
1456
|
+
menu.style.left = (rect.left - menuRect.width - 6) + "px";
|
|
1457
|
+
}
|
|
1458
|
+
if (menuRect.bottom > window.innerHeight - 8) {
|
|
1459
|
+
menu.style.top = (window.innerHeight - menuRect.height - 8) + "px";
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// Close on outside click
|
|
1464
|
+
setTimeout(function () {
|
|
1465
|
+
document.addEventListener("click", handleUserCtxOutsideClick, true);
|
|
1466
|
+
}, 0);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function handleUserCtxOutsideClick(e) {
|
|
1470
|
+
if (userCtxMenu && !userCtxMenu.contains(e.target)) {
|
|
1471
|
+
closeUserCtxMenu();
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1390
1475
|
// --- Project context menu ---
|
|
1391
1476
|
var projectCtxMenu = null;
|
|
1392
1477
|
|
|
@@ -1601,8 +1686,9 @@ function closeProjectCtxMenu() {
|
|
|
1601
1686
|
}
|
|
1602
1687
|
}
|
|
1603
1688
|
|
|
1604
|
-
function showIconCtxMenu(anchorEl, slug) {
|
|
1689
|
+
function showIconCtxMenu(anchorEl, slug, name) {
|
|
1605
1690
|
closeProjectCtxMenu();
|
|
1691
|
+
closeUserCtxMenu();
|
|
1606
1692
|
closeEmojiPicker();
|
|
1607
1693
|
|
|
1608
1694
|
var menu = document.createElement("div");
|
|
@@ -1618,6 +1704,24 @@ function showIconCtxMenu(anchorEl, slug) {
|
|
|
1618
1704
|
});
|
|
1619
1705
|
menu.appendChild(iconItem);
|
|
1620
1706
|
|
|
1707
|
+
// --- Separator ---
|
|
1708
|
+
var sep = document.createElement("div");
|
|
1709
|
+
sep.className = "project-ctx-separator";
|
|
1710
|
+
menu.appendChild(sep);
|
|
1711
|
+
|
|
1712
|
+
// --- Remove Project ---
|
|
1713
|
+
var removeItem = document.createElement("button");
|
|
1714
|
+
removeItem.className = "project-ctx-item project-ctx-delete";
|
|
1715
|
+
removeItem.innerHTML = iconHtml("trash-2") + " <span>Remove Project</span>";
|
|
1716
|
+
removeItem.addEventListener("click", function (e) {
|
|
1717
|
+
e.stopPropagation();
|
|
1718
|
+
closeProjectCtxMenu();
|
|
1719
|
+
if (ctx.ws && ctx.connected) {
|
|
1720
|
+
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug, name: name || slug }));
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
menu.appendChild(removeItem);
|
|
1724
|
+
|
|
1621
1725
|
document.body.appendChild(menu);
|
|
1622
1726
|
projectCtxMenu = menu;
|
|
1623
1727
|
refreshIcons();
|
|
@@ -1639,6 +1743,7 @@ function showIconCtxMenu(anchorEl, slug) {
|
|
|
1639
1743
|
|
|
1640
1744
|
function showProjectCtxMenu(anchorEl, slug, name, icon, position) {
|
|
1641
1745
|
closeProjectCtxMenu();
|
|
1746
|
+
closeUserCtxMenu();
|
|
1642
1747
|
closeEmojiPicker();
|
|
1643
1748
|
|
|
1644
1749
|
var menu = document.createElement("div");
|
|
@@ -1910,10 +2015,6 @@ function showTrashZone() {
|
|
|
1910
2015
|
trash.classList.remove("drag-hover");
|
|
1911
2016
|
var slug = e.dataTransfer.getData("text/plain");
|
|
1912
2017
|
if (slug && ctx.ws && ctx.connected) {
|
|
1913
|
-
// Spawn dust particles at trash position
|
|
1914
|
-
var rect = trash.getBoundingClientRect();
|
|
1915
|
-
spawnDustParticles(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
1916
|
-
// Check for tasks before removing
|
|
1917
2018
|
ctx.ws.send(JSON.stringify({ type: "remove_project_check", slug: slug }));
|
|
1918
2019
|
}
|
|
1919
2020
|
});
|
|
@@ -1926,7 +2027,7 @@ function hideTrashZone() {
|
|
|
1926
2027
|
if (addBtn) addBtn.style.display = "";
|
|
1927
2028
|
}
|
|
1928
2029
|
|
|
1929
|
-
function spawnDustParticles(cx, cy) {
|
|
2030
|
+
export function spawnDustParticles(cx, cy) {
|
|
1930
2031
|
var colors = ["#8B7355", "#A0522D", "#D2B48C", "#C4A882", "#9E9E9E", "#B8860B", "#BC8F8F"];
|
|
1931
2032
|
var count = 24;
|
|
1932
2033
|
var container = document.createElement("div");
|
|
@@ -2113,6 +2214,15 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2113
2214
|
if (p.isProcessing) statusDot.classList.add("processing");
|
|
2114
2215
|
el.appendChild(statusDot);
|
|
2115
2216
|
|
|
2217
|
+
// Unread badge (top-right)
|
|
2218
|
+
var projectBadge = document.createElement("span");
|
|
2219
|
+
projectBadge.className = "icon-strip-project-badge";
|
|
2220
|
+
if (p.unread > 0 && !isActive) {
|
|
2221
|
+
projectBadge.textContent = p.unread > 99 ? "99+" : String(p.unread);
|
|
2222
|
+
projectBadge.classList.add("has-unread");
|
|
2223
|
+
}
|
|
2224
|
+
el.appendChild(projectBadge);
|
|
2225
|
+
|
|
2116
2226
|
// Tooltip on hover
|
|
2117
2227
|
(function (name, elem) {
|
|
2118
2228
|
elem.addEventListener("mouseenter", function () { showIconTooltip(elem, name); });
|
|
@@ -2128,13 +2238,13 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2128
2238
|
})(p.slug);
|
|
2129
2239
|
|
|
2130
2240
|
// Right-click context menu (icon only)
|
|
2131
|
-
(function (slug, elem) {
|
|
2241
|
+
(function (slug, name, elem) {
|
|
2132
2242
|
elem.addEventListener("contextmenu", function (e) {
|
|
2133
2243
|
e.preventDefault();
|
|
2134
2244
|
e.stopPropagation();
|
|
2135
|
-
showIconCtxMenu(elem, slug);
|
|
2245
|
+
showIconCtxMenu(elem, slug, name);
|
|
2136
2246
|
});
|
|
2137
|
-
})(p.slug, el);
|
|
2247
|
+
})(p.slug, p.name || p.slug, el);
|
|
2138
2248
|
|
|
2139
2249
|
// Drag-and-drop reordering
|
|
2140
2250
|
setupDragHandlers(el, p.slug);
|
|
@@ -2197,24 +2307,46 @@ export function getEmojiCategories() { return EMOJI_CATEGORIES; }
|
|
|
2197
2307
|
// --- User strip (DM targets) ---
|
|
2198
2308
|
var cachedAllUsers = [];
|
|
2199
2309
|
var cachedOnlineUserIds = [];
|
|
2310
|
+
var cachedDmFavorites = [];
|
|
2311
|
+
var cachedDmConversations = [];
|
|
2312
|
+
var cachedDmUnread = {};
|
|
2313
|
+
var cachedMyUserId = null;
|
|
2200
2314
|
var currentDmUserId = null;
|
|
2315
|
+
var dmPickerOpen = false;
|
|
2201
2316
|
|
|
2202
|
-
|
|
2317
|
+
var cachedDmRemovedUsers = {};
|
|
2318
|
+
|
|
2319
|
+
export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites, dmConversations, dmUnread, dmRemovedUsers) {
|
|
2203
2320
|
cachedAllUsers = allUsers || [];
|
|
2204
2321
|
cachedOnlineUserIds = onlineUserIds || [];
|
|
2322
|
+
cachedDmFavorites = dmFavorites || [];
|
|
2323
|
+
cachedDmConversations = dmConversations || [];
|
|
2324
|
+
cachedDmUnread = dmUnread || {};
|
|
2325
|
+
cachedDmRemovedUsers = dmRemovedUsers || {};
|
|
2326
|
+
cachedMyUserId = myUserId;
|
|
2205
2327
|
var container = document.getElementById("icon-strip-users");
|
|
2206
2328
|
if (!container) return;
|
|
2207
2329
|
|
|
2208
|
-
//
|
|
2209
|
-
var
|
|
2330
|
+
// All other users
|
|
2331
|
+
var allOthers = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
|
|
2210
2332
|
|
|
2211
2333
|
// Hide section if no other users (single-user mode or alone)
|
|
2212
|
-
if (
|
|
2334
|
+
if (allOthers.length === 0) {
|
|
2213
2335
|
container.innerHTML = "";
|
|
2214
2336
|
container.classList.add("hidden");
|
|
2215
2337
|
return;
|
|
2216
2338
|
}
|
|
2217
2339
|
|
|
2340
|
+
// Filter to show only: favorites + users with unread + users with DM conversations
|
|
2341
|
+
// But exclude users explicitly removed from favorites
|
|
2342
|
+
var others = allOthers.filter(function (u) {
|
|
2343
|
+
if (cachedDmRemovedUsers[u.id]) return false;
|
|
2344
|
+
if (cachedDmFavorites.indexOf(u.id) !== -1) return true;
|
|
2345
|
+
if (cachedDmUnread[u.id] && cachedDmUnread[u.id] > 0) return true;
|
|
2346
|
+
if (cachedDmConversations.indexOf(u.id) !== -1) return true;
|
|
2347
|
+
return false;
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2218
2350
|
container.classList.remove("hidden");
|
|
2219
2351
|
container.innerHTML = "";
|
|
2220
2352
|
|
|
@@ -2254,21 +2386,141 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId) {
|
|
|
2254
2386
|
if (ctx.openDm) ctx.openDm(u.id);
|
|
2255
2387
|
});
|
|
2256
2388
|
|
|
2389
|
+
// Right-click: show context menu
|
|
2390
|
+
el.addEventListener("contextmenu", function (e) {
|
|
2391
|
+
e.preventDefault();
|
|
2392
|
+
e.stopPropagation();
|
|
2393
|
+
showUserCtxMenu(el, u);
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2257
2396
|
container.appendChild(el);
|
|
2258
2397
|
})(others[i]);
|
|
2259
2398
|
}
|
|
2260
2399
|
|
|
2261
|
-
//
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2400
|
+
// Add user (+) button
|
|
2401
|
+
var addBtn = document.createElement("button");
|
|
2402
|
+
addBtn.className = "icon-strip-invite";
|
|
2403
|
+
addBtn.innerHTML = iconHtml("user-plus");
|
|
2404
|
+
addBtn.addEventListener("click", function (e) {
|
|
2405
|
+
e.stopPropagation();
|
|
2406
|
+
toggleDmUserPicker(addBtn);
|
|
2407
|
+
});
|
|
2408
|
+
addBtn.addEventListener("mouseenter", function () { showIconTooltip(addBtn, "Add DM favorite"); });
|
|
2409
|
+
addBtn.addEventListener("mouseleave", hideIconTooltip);
|
|
2410
|
+
container.appendChild(addBtn);
|
|
2269
2411
|
refreshIcons();
|
|
2270
2412
|
}
|
|
2271
2413
|
|
|
2414
|
+
function toggleDmUserPicker(anchorEl) {
|
|
2415
|
+
if (dmPickerOpen) {
|
|
2416
|
+
closeDmUserPicker();
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
dmPickerOpen = true;
|
|
2420
|
+
|
|
2421
|
+
var picker = document.createElement("div");
|
|
2422
|
+
picker.className = "dm-user-picker";
|
|
2423
|
+
picker.id = "dm-user-picker";
|
|
2424
|
+
|
|
2425
|
+
// Search input
|
|
2426
|
+
var searchInput = document.createElement("input");
|
|
2427
|
+
searchInput.className = "dm-user-picker-search";
|
|
2428
|
+
searchInput.type = "text";
|
|
2429
|
+
searchInput.placeholder = "Search users...";
|
|
2430
|
+
picker.appendChild(searchInput);
|
|
2431
|
+
|
|
2432
|
+
// Scrollable list
|
|
2433
|
+
var listEl = document.createElement("div");
|
|
2434
|
+
listEl.className = "dm-user-picker-list";
|
|
2435
|
+
picker.appendChild(listEl);
|
|
2436
|
+
|
|
2437
|
+
// Position the picker above the + button
|
|
2438
|
+
document.body.appendChild(picker);
|
|
2439
|
+
var rect = anchorEl.getBoundingClientRect();
|
|
2440
|
+
picker.style.left = (rect.right + 8) + "px";
|
|
2441
|
+
picker.style.bottom = (window.innerHeight - rect.bottom) + "px";
|
|
2442
|
+
|
|
2443
|
+
function renderPickerList(filter) {
|
|
2444
|
+
listEl.innerHTML = "";
|
|
2445
|
+
var allOthers = cachedAllUsers.filter(function (u) { return u.id !== cachedMyUserId; });
|
|
2446
|
+
// Exclude already-favorited users
|
|
2447
|
+
var available = allOthers.filter(function (u) {
|
|
2448
|
+
return cachedDmFavorites.indexOf(u.id) === -1;
|
|
2449
|
+
});
|
|
2450
|
+
if (filter) {
|
|
2451
|
+
var lf = filter.toLowerCase();
|
|
2452
|
+
available = available.filter(function (u) {
|
|
2453
|
+
return (u.displayName && u.displayName.toLowerCase().indexOf(lf) !== -1) ||
|
|
2454
|
+
(u.username && u.username.toLowerCase().indexOf(lf) !== -1);
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
if (available.length === 0) {
|
|
2458
|
+
var emptyEl = document.createElement("div");
|
|
2459
|
+
emptyEl.className = "dm-user-picker-empty";
|
|
2460
|
+
emptyEl.textContent = filter ? "No users found" : "No more users to add";
|
|
2461
|
+
listEl.appendChild(emptyEl);
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
for (var i = 0; i < available.length; i++) {
|
|
2465
|
+
(function (u) {
|
|
2466
|
+
var item = document.createElement("div");
|
|
2467
|
+
item.className = "dm-user-picker-item";
|
|
2468
|
+
|
|
2469
|
+
var av = document.createElement("img");
|
|
2470
|
+
av.className = "dm-user-picker-avatar";
|
|
2471
|
+
av.src = "https://api.dicebear.com/9.x/" + (u.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(u.avatarSeed || u.username) + "&size=28";
|
|
2472
|
+
av.alt = u.displayName;
|
|
2473
|
+
item.appendChild(av);
|
|
2474
|
+
|
|
2475
|
+
var name = document.createElement("span");
|
|
2476
|
+
name.className = "dm-user-picker-name";
|
|
2477
|
+
name.textContent = u.displayName;
|
|
2478
|
+
item.appendChild(name);
|
|
2479
|
+
|
|
2480
|
+
item.addEventListener("click", function () {
|
|
2481
|
+
if (ctx.sendWs) {
|
|
2482
|
+
ctx.sendWs({ type: "dm_add_favorite", targetUserId: u.id });
|
|
2483
|
+
}
|
|
2484
|
+
closeDmUserPicker();
|
|
2485
|
+
});
|
|
2486
|
+
|
|
2487
|
+
listEl.appendChild(item);
|
|
2488
|
+
})(available[i]);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
renderPickerList("");
|
|
2493
|
+
searchInput.addEventListener("input", function () {
|
|
2494
|
+
renderPickerList(searchInput.value);
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
// Focus search
|
|
2498
|
+
setTimeout(function () { searchInput.focus(); }, 50);
|
|
2499
|
+
|
|
2500
|
+
// Close on click outside
|
|
2501
|
+
function onDocClick(e) {
|
|
2502
|
+
if (!picker.contains(e.target) && e.target !== anchorEl && !anchorEl.contains(e.target)) {
|
|
2503
|
+
closeDmUserPicker();
|
|
2504
|
+
document.removeEventListener("click", onDocClick, true);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
setTimeout(function () {
|
|
2508
|
+
document.addEventListener("click", onDocClick, true);
|
|
2509
|
+
}, 10);
|
|
2510
|
+
picker._docClickHandler = onDocClick;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
export function closeDmUserPicker() {
|
|
2514
|
+
dmPickerOpen = false;
|
|
2515
|
+
var picker = document.getElementById("dm-user-picker");
|
|
2516
|
+
if (picker) {
|
|
2517
|
+
if (picker._docClickHandler) {
|
|
2518
|
+
document.removeEventListener("click", picker._docClickHandler, true);
|
|
2519
|
+
}
|
|
2520
|
+
picker.remove();
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2272
2524
|
export function setCurrentDmUser(userId) {
|
|
2273
2525
|
currentDmUserId = userId;
|
|
2274
2526
|
// Update active state on user icons immediately
|
|
@@ -2296,6 +2548,32 @@ export function updateDmBadge(userId, count) {
|
|
|
2296
2548
|
}
|
|
2297
2549
|
}
|
|
2298
2550
|
|
|
2551
|
+
export function updateSessionBadge(sessionId, count) {
|
|
2552
|
+
var badge = document.querySelector('.session-unread-badge[data-session-id="' + sessionId + '"]');
|
|
2553
|
+
if (!badge) return;
|
|
2554
|
+
if (count > 0) {
|
|
2555
|
+
badge.textContent = count > 99 ? "99+" : String(count);
|
|
2556
|
+
badge.classList.add("has-unread");
|
|
2557
|
+
} else {
|
|
2558
|
+
badge.textContent = "";
|
|
2559
|
+
badge.classList.remove("has-unread");
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
export function updateProjectBadge(slug, count) {
|
|
2564
|
+
var icon = document.querySelector('.icon-strip-item[data-slug="' + slug + '"]');
|
|
2565
|
+
if (!icon) return;
|
|
2566
|
+
var badge = icon.querySelector(".icon-strip-project-badge");
|
|
2567
|
+
if (!badge) return;
|
|
2568
|
+
if (count > 0) {
|
|
2569
|
+
badge.textContent = count > 99 ? "99+" : String(count);
|
|
2570
|
+
badge.classList.add("has-unread");
|
|
2571
|
+
} else {
|
|
2572
|
+
badge.textContent = "";
|
|
2573
|
+
badge.classList.remove("has-unread");
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2299
2577
|
export function initIconStrip(_ctx) {
|
|
2300
2578
|
var addBtn = document.getElementById("icon-strip-add");
|
|
2301
2579
|
if (addBtn) {
|
|
@@ -156,7 +156,7 @@ function dismissOnboarding() {
|
|
|
156
156
|
|
|
157
157
|
// --- Visibility ---
|
|
158
158
|
|
|
159
|
-
function showNotes() {
|
|
159
|
+
export function showNotes() {
|
|
160
160
|
notesVisible = true;
|
|
161
161
|
var container = document.getElementById("sticky-notes-container");
|
|
162
162
|
var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
|
|
@@ -164,7 +164,7 @@ function showNotes() {
|
|
|
164
164
|
if (toggleBtn) toggleBtn.classList.add("active");
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
function hideNotes() {
|
|
167
|
+
export function hideNotes() {
|
|
168
168
|
notesVisible = false;
|
|
169
169
|
var container = document.getElementById("sticky-notes-container");
|
|
170
170
|
var toggleBtn = document.getElementById("sticky-notes-toggle-btn");
|
|
@@ -260,7 +260,7 @@ function renderMiniMarkdown(text) {
|
|
|
260
260
|
|
|
261
261
|
function syncTitle(noteEl, text) {
|
|
262
262
|
var spacer = noteEl.querySelector(".sticky-note-spacer");
|
|
263
|
-
if (spacer) spacer.textContent = getTitle(text);
|
|
263
|
+
if (spacer) spacer.textContent = getTitle(text) || "Untitled";
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
// --- HTML-to-Markdown reverse conversion (for contenteditable) ---
|
|
@@ -356,7 +356,7 @@ function renderNote(data) {
|
|
|
356
356
|
|
|
357
357
|
var spacer = document.createElement("div");
|
|
358
358
|
spacer.className = "sticky-note-spacer";
|
|
359
|
-
spacer.textContent = getTitle(data.text);
|
|
359
|
+
spacer.textContent = getTitle(data.text) || "Untitled";
|
|
360
360
|
header.appendChild(spacer);
|
|
361
361
|
|
|
362
362
|
var addBtn = document.createElement("button");
|
|
@@ -644,7 +644,25 @@ function showFormatToolbar(rendered) {
|
|
|
644
644
|
if (!sel.toString().trim()) return;
|
|
645
645
|
|
|
646
646
|
var range = sel.getRangeAt(0);
|
|
647
|
-
|
|
647
|
+
// When dragging outside the note, commonAncestorContainer may be a parent
|
|
648
|
+
// of rendered. Clamp the range to the rendered element so the toolbar shows.
|
|
649
|
+
if (!rendered.contains(range.commonAncestorContainer)) {
|
|
650
|
+
try {
|
|
651
|
+
var clampedRange = range.cloneRange();
|
|
652
|
+
if (range.startContainer === rendered || rendered.contains(range.startContainer)) {
|
|
653
|
+
clampedRange.selectNodeContents(rendered);
|
|
654
|
+
clampedRange.setStart(range.startContainer, range.startOffset);
|
|
655
|
+
} else if (range.endContainer === rendered || rendered.contains(range.endContainer)) {
|
|
656
|
+
clampedRange.selectNodeContents(rendered);
|
|
657
|
+
clampedRange.setEnd(range.endContainer, range.endOffset);
|
|
658
|
+
} else {
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
range = clampedRange;
|
|
662
|
+
} catch (e) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
648
666
|
|
|
649
667
|
var toolbar = document.createElement("div");
|
|
650
668
|
toolbar.className = "sn-format-toolbar";
|
|
@@ -1251,3 +1269,7 @@ export function closeArchive() {
|
|
|
1251
1269
|
export function isArchiveOpen() {
|
|
1252
1270
|
return archiveOpen;
|
|
1253
1271
|
}
|
|
1272
|
+
|
|
1273
|
+
export function isNotesVisible() {
|
|
1274
|
+
return notesVisible;
|
|
1275
|
+
}
|