clay-server 2.10.0 → 2.11.0-beta.1
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/lib/dm.js +135 -0
- package/lib/project.js +8 -0
- package/lib/public/app.js +284 -2
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/messages.css +174 -0
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +6 -0
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/sidebar.js +105 -3
- package/lib/server.js +102 -0
- package/lib/users.js +7 -0
- package/package.json +1 -1
package/lib/dm.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var config = require("./config");
|
|
4
|
+
|
|
5
|
+
var DM_DIR = path.join(config.CONFIG_DIR, "dm");
|
|
6
|
+
|
|
7
|
+
// Ensure dm directory exists
|
|
8
|
+
function ensureDmDir() {
|
|
9
|
+
fs.mkdirSync(DM_DIR, { recursive: true });
|
|
10
|
+
config.chmodSafe(DM_DIR, 0o700);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Generate deterministic DM key from two user IDs (sorted, order-independent)
|
|
14
|
+
function dmKey(userId1, userId2) {
|
|
15
|
+
return [userId1, userId2].sort().join(":");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// File path for a DM conversation
|
|
19
|
+
function dmFilePath(key) {
|
|
20
|
+
// Replace : with _ for safe filename
|
|
21
|
+
return path.join(DM_DIR, key.replace(/:/g, "_") + ".jsonl");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Load DM history from JSONL file
|
|
25
|
+
function loadHistory(key) {
|
|
26
|
+
var filePath = dmFilePath(key);
|
|
27
|
+
if (!fs.existsSync(filePath)) return [];
|
|
28
|
+
try {
|
|
29
|
+
var content = fs.readFileSync(filePath, "utf8").trim();
|
|
30
|
+
if (!content) return [];
|
|
31
|
+
var lines = content.split("\n");
|
|
32
|
+
var messages = [];
|
|
33
|
+
for (var i = 0; i < lines.length; i++) {
|
|
34
|
+
if (!lines[i].trim()) continue;
|
|
35
|
+
try {
|
|
36
|
+
messages.push(JSON.parse(lines[i]));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// skip malformed lines
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return messages;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Append a message to DM JSONL file
|
|
48
|
+
function appendMessage(key, message) {
|
|
49
|
+
ensureDmDir();
|
|
50
|
+
var filePath = dmFilePath(key);
|
|
51
|
+
var line = JSON.stringify(message) + "\n";
|
|
52
|
+
fs.appendFileSync(filePath, line);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Open a DM conversation (find or create)
|
|
56
|
+
function openDm(userId1, userId2) {
|
|
57
|
+
var key = dmKey(userId1, userId2);
|
|
58
|
+
var history = loadHistory(key);
|
|
59
|
+
return { dmKey: key, messages: history };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Send a DM message
|
|
63
|
+
function sendMessage(key, fromUserId, text) {
|
|
64
|
+
var message = {
|
|
65
|
+
type: "dm_message",
|
|
66
|
+
ts: Date.now(),
|
|
67
|
+
from: fromUserId,
|
|
68
|
+
text: text,
|
|
69
|
+
};
|
|
70
|
+
appendMessage(key, message);
|
|
71
|
+
return message;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Get list of all DM conversations for a user
|
|
75
|
+
// Returns: [{ dmKey, otherUserId, lastMessage, lastTs }]
|
|
76
|
+
function getDmList(userId) {
|
|
77
|
+
ensureDmDir();
|
|
78
|
+
var files;
|
|
79
|
+
try {
|
|
80
|
+
files = fs.readdirSync(DM_DIR).filter(function (f) {
|
|
81
|
+
return f.endsWith(".jsonl");
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var dms = [];
|
|
88
|
+
for (var i = 0; i < files.length; i++) {
|
|
89
|
+
// Reconstruct dmKey from filename (replace _ back to :)
|
|
90
|
+
var key = files[i].replace(".jsonl", "").replace(/_/g, ":");
|
|
91
|
+
var parts = key.split(":");
|
|
92
|
+
if (parts.length !== 2) continue;
|
|
93
|
+
|
|
94
|
+
// Check if this user is a participant
|
|
95
|
+
var idx = parts.indexOf(userId);
|
|
96
|
+
if (idx === -1) continue;
|
|
97
|
+
|
|
98
|
+
var otherUserId = parts[idx === 0 ? 1 : 0];
|
|
99
|
+
|
|
100
|
+
// Get last message
|
|
101
|
+
var messages = loadHistory(key);
|
|
102
|
+
var lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
103
|
+
|
|
104
|
+
dms.push({
|
|
105
|
+
dmKey: key,
|
|
106
|
+
otherUserId: otherUserId,
|
|
107
|
+
lastMessage: lastMessage ? lastMessage.text : null,
|
|
108
|
+
lastTs: lastMessage ? lastMessage.ts : 0,
|
|
109
|
+
messageCount: messages.length,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Sort by most recent activity
|
|
114
|
+
dms.sort(function (a, b) {
|
|
115
|
+
return b.lastTs - a.lastTs;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return dms;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extension point: check if a user is a mate (AI persona)
|
|
122
|
+
// Returns false for now - will be implemented when Mates feature is added
|
|
123
|
+
function isMate(userId) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
dmKey: dmKey,
|
|
129
|
+
openDm: openDm,
|
|
130
|
+
sendMessage: sendMessage,
|
|
131
|
+
getDmList: getDmList,
|
|
132
|
+
loadHistory: loadHistory,
|
|
133
|
+
isMate: isMate,
|
|
134
|
+
DM_DIR: DM_DIR,
|
|
135
|
+
};
|
package/lib/project.js
CHANGED
|
@@ -1137,6 +1137,14 @@ function createProjectContext(opts) {
|
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
1139
|
function handleMessage(ws, msg) {
|
|
1140
|
+
// --- DM messages (delegated to server-level handler) ---
|
|
1141
|
+
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing") {
|
|
1142
|
+
if (typeof opts.onDmMessage === "function") {
|
|
1143
|
+
opts.onDmMessage(ws, msg);
|
|
1144
|
+
}
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1140
1148
|
if (msg.type === "push_subscribe") {
|
|
1141
1149
|
if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
|
|
1142
1150
|
return;
|
package/lib/public/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
|
|
2
2
|
import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
|
|
4
|
-
import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories } from './modules/sidebar.js';
|
|
4
|
+
import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge } from './modules/sidebar.js';
|
|
5
5
|
import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
|
|
6
6
|
import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
|
|
7
7
|
import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands, sendMessage } from './modules/input.js';
|
|
@@ -49,6 +49,13 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
49
49
|
var imagePreviewBar = $("image-preview-bar");
|
|
50
50
|
var connectOverlay = $("connect-overlay");
|
|
51
51
|
|
|
52
|
+
// --- DM Mode ---
|
|
53
|
+
var dmMode = false;
|
|
54
|
+
var dmKey = null;
|
|
55
|
+
var dmTargetUser = null;
|
|
56
|
+
var dmUnread = {}; // { otherUserId: count }
|
|
57
|
+
var cachedAllUsers = [];
|
|
58
|
+
|
|
52
59
|
// --- Home Hub ---
|
|
53
60
|
var homeHub = $("home-hub");
|
|
54
61
|
var homeHubVisible = false;
|
|
@@ -524,9 +531,232 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
524
531
|
}
|
|
525
532
|
}
|
|
526
533
|
|
|
534
|
+
// --- DM Mode Functions ---
|
|
535
|
+
function openDm(targetUserId) {
|
|
536
|
+
if (!ws || ws.readyState !== 1) return;
|
|
537
|
+
ws.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function enterDmMode(key, targetUser, messages) {
|
|
541
|
+
dmMode = true;
|
|
542
|
+
dmKey = key;
|
|
543
|
+
dmTargetUser = targetUser;
|
|
544
|
+
|
|
545
|
+
// Clear unread for this user
|
|
546
|
+
if (targetUser) {
|
|
547
|
+
dmUnread[targetUser.id] = 0;
|
|
548
|
+
updateDmBadge(targetUser.id, 0);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Update icon strip active state
|
|
552
|
+
setCurrentDmUser(targetUser ? targetUser.id : null);
|
|
553
|
+
var activeProj = document.querySelector("#icon-strip-projects .icon-strip-item.active");
|
|
554
|
+
if (activeProj) activeProj.classList.remove("active");
|
|
555
|
+
var homeIcon = document.querySelector(".icon-strip-home");
|
|
556
|
+
if (homeIcon) homeIcon.classList.remove("active");
|
|
557
|
+
// Re-render user strip to show active state
|
|
558
|
+
if (cachedProjects && cachedProjects.length > 0) {
|
|
559
|
+
renderProjectList();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Hide home hub if visible
|
|
563
|
+
hideHomeHub();
|
|
564
|
+
|
|
565
|
+
// Hide project UI + sidebar, show DM UI
|
|
566
|
+
var mainCol = document.getElementById("main-column");
|
|
567
|
+
if (mainCol) mainCol.classList.add("dm-mode");
|
|
568
|
+
var sidebarCol = document.getElementById("sidebar-column");
|
|
569
|
+
if (sidebarCol) sidebarCol.classList.add("dm-mode");
|
|
570
|
+
var resizeHandle = document.getElementById("sidebar-resize-handle");
|
|
571
|
+
if (resizeHandle) resizeHandle.classList.add("dm-mode");
|
|
572
|
+
|
|
573
|
+
// Hide user-island (my avatar behind it becomes visible)
|
|
574
|
+
var userIsland = document.getElementById("user-island");
|
|
575
|
+
if (userIsland) userIsland.classList.add("dm-hidden");
|
|
576
|
+
|
|
577
|
+
// Render DM messages
|
|
578
|
+
messagesEl.innerHTML = "";
|
|
579
|
+
if (messages && messages.length > 0) {
|
|
580
|
+
for (var i = 0; i < messages.length; i++) {
|
|
581
|
+
appendDmMessage(messages[i]);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
scrollToBottom();
|
|
585
|
+
|
|
586
|
+
// Focus input
|
|
587
|
+
if (inputEl) {
|
|
588
|
+
inputEl.placeholder = "Message " + (targetUser ? targetUser.displayName : "");
|
|
589
|
+
inputEl.focus();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Populate DM header bar with user avatar, name, and personal color
|
|
593
|
+
if (targetUser) {
|
|
594
|
+
var dmHeaderBar = document.getElementById("dm-header-bar");
|
|
595
|
+
var dmAvatar = document.getElementById("dm-header-avatar");
|
|
596
|
+
var dmName = document.getElementById("dm-header-name");
|
|
597
|
+
if (dmAvatar) {
|
|
598
|
+
dmAvatar.src = "https://api.dicebear.com/9.x/" + (targetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(targetUser.avatarSeed || targetUser.username) + "&size=28";
|
|
599
|
+
}
|
|
600
|
+
if (dmName) dmName.textContent = targetUser.displayName;
|
|
601
|
+
if (dmHeaderBar && targetUser.avatarColor) {
|
|
602
|
+
dmHeaderBar.style.background = targetUser.avatarColor;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function exitDmMode() {
|
|
608
|
+
if (!dmMode) return;
|
|
609
|
+
dmMode = false;
|
|
610
|
+
dmKey = null;
|
|
611
|
+
dmTargetUser = null;
|
|
612
|
+
setCurrentDmUser(null);
|
|
613
|
+
|
|
614
|
+
var mainCol = document.getElementById("main-column");
|
|
615
|
+
if (mainCol) mainCol.classList.remove("dm-mode");
|
|
616
|
+
var sidebarCol = document.getElementById("sidebar-column");
|
|
617
|
+
if (sidebarCol) sidebarCol.classList.remove("dm-mode");
|
|
618
|
+
var resizeHandle = document.getElementById("sidebar-resize-handle");
|
|
619
|
+
if (resizeHandle) resizeHandle.classList.remove("dm-mode");
|
|
620
|
+
|
|
621
|
+
// Reset DM header
|
|
622
|
+
var dmHeaderBar = document.getElementById("dm-header-bar");
|
|
623
|
+
if (dmHeaderBar) dmHeaderBar.style.background = "";
|
|
624
|
+
|
|
625
|
+
// Restore user-island (covers my avatar again)
|
|
626
|
+
var userIsland = document.getElementById("user-island");
|
|
627
|
+
if (userIsland) userIsland.classList.remove("dm-hidden");
|
|
628
|
+
|
|
629
|
+
// Restore project UI
|
|
630
|
+
if (inputEl) inputEl.placeholder = "";
|
|
631
|
+
renderProjectList();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function appendDmMessage(msg) {
|
|
635
|
+
var isMe = msg.from === myUserId;
|
|
636
|
+
var d = new Date(msg.ts);
|
|
637
|
+
var timeStr = d.getHours().toString().padStart(2, "0") + ":" + d.getMinutes().toString().padStart(2, "0");
|
|
638
|
+
|
|
639
|
+
// Check if we can compact (same sender as previous, within 5 min)
|
|
640
|
+
var prev = messagesEl.lastElementChild;
|
|
641
|
+
var compact = false;
|
|
642
|
+
if (prev && prev.dataset.from === msg.from) {
|
|
643
|
+
var prevTs = parseInt(prev.dataset.ts || "0", 10);
|
|
644
|
+
if (msg.ts - prevTs < 300000) compact = true;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
var div = document.createElement("div");
|
|
648
|
+
div.className = "dm-msg" + (compact ? " dm-msg-compact" : "");
|
|
649
|
+
div.dataset.from = msg.from;
|
|
650
|
+
div.dataset.ts = msg.ts;
|
|
651
|
+
|
|
652
|
+
if (compact) {
|
|
653
|
+
// Compact: just hover-time + text, no avatar/name
|
|
654
|
+
var hoverTime = document.createElement("span");
|
|
655
|
+
hoverTime.className = "dm-msg-hover-time";
|
|
656
|
+
hoverTime.textContent = timeStr;
|
|
657
|
+
div.appendChild(hoverTime);
|
|
658
|
+
|
|
659
|
+
var body = document.createElement("div");
|
|
660
|
+
body.className = "dm-msg-body";
|
|
661
|
+
body.textContent = msg.text;
|
|
662
|
+
div.appendChild(body);
|
|
663
|
+
} else {
|
|
664
|
+
// Full: avatar + header(name, time) + text
|
|
665
|
+
var avatar = document.createElement("img");
|
|
666
|
+
avatar.className = "dm-msg-avatar";
|
|
667
|
+
if (isMe) {
|
|
668
|
+
var myUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
|
|
669
|
+
var myStyle = myUser ? myUser.avatarStyle : "thumbs";
|
|
670
|
+
var mySeed = myUser ? (myUser.avatarSeed || myUser.username) : myUserId;
|
|
671
|
+
avatar.src = "https://api.dicebear.com/9.x/" + (myStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(mySeed) + "&size=36";
|
|
672
|
+
} else if (dmTargetUser) {
|
|
673
|
+
avatar.src = "https://api.dicebear.com/9.x/" + (dmTargetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(dmTargetUser.avatarSeed || dmTargetUser.username) + "&size=36";
|
|
674
|
+
}
|
|
675
|
+
div.appendChild(avatar);
|
|
676
|
+
|
|
677
|
+
var content = document.createElement("div");
|
|
678
|
+
content.className = "dm-msg-content";
|
|
679
|
+
|
|
680
|
+
var header = document.createElement("div");
|
|
681
|
+
header.className = "dm-msg-header";
|
|
682
|
+
|
|
683
|
+
var name = document.createElement("span");
|
|
684
|
+
name.className = "dm-msg-name";
|
|
685
|
+
if (isMe) {
|
|
686
|
+
var mu = cachedAllUsers.find(function (u) { return u.id === myUserId; });
|
|
687
|
+
name.textContent = mu ? mu.displayName : "Me";
|
|
688
|
+
} else {
|
|
689
|
+
name.textContent = dmTargetUser ? dmTargetUser.displayName : "User";
|
|
690
|
+
}
|
|
691
|
+
header.appendChild(name);
|
|
692
|
+
|
|
693
|
+
var time = document.createElement("span");
|
|
694
|
+
time.className = "dm-msg-time";
|
|
695
|
+
time.textContent = timeStr;
|
|
696
|
+
header.appendChild(time);
|
|
697
|
+
|
|
698
|
+
content.appendChild(header);
|
|
699
|
+
|
|
700
|
+
var body = document.createElement("div");
|
|
701
|
+
body.className = "dm-msg-body";
|
|
702
|
+
body.textContent = msg.text;
|
|
703
|
+
content.appendChild(body);
|
|
704
|
+
|
|
705
|
+
div.appendChild(content);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
messagesEl.appendChild(div);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
var dmTypingTimer = null;
|
|
712
|
+
|
|
713
|
+
function showDmTypingIndicator(typing) {
|
|
714
|
+
var existing = document.getElementById("dm-typing-indicator");
|
|
715
|
+
if (!typing) {
|
|
716
|
+
if (existing) existing.remove();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (existing) return; // already showing
|
|
720
|
+
if (!dmTargetUser) return;
|
|
721
|
+
|
|
722
|
+
var div = document.createElement("div");
|
|
723
|
+
div.id = "dm-typing-indicator";
|
|
724
|
+
div.className = "dm-msg dm-typing-indicator";
|
|
725
|
+
|
|
726
|
+
var avatar = document.createElement("img");
|
|
727
|
+
avatar.className = "dm-msg-avatar";
|
|
728
|
+
avatar.src = "https://api.dicebear.com/9.x/" + (dmTargetUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(dmTargetUser.avatarSeed || dmTargetUser.username) + "&size=36";
|
|
729
|
+
div.appendChild(avatar);
|
|
730
|
+
|
|
731
|
+
var dots = document.createElement("div");
|
|
732
|
+
dots.className = "dm-typing-dots";
|
|
733
|
+
dots.innerHTML = "<span></span><span></span><span></span>";
|
|
734
|
+
div.appendChild(dots);
|
|
735
|
+
|
|
736
|
+
messagesEl.appendChild(div);
|
|
737
|
+
scrollToBottom();
|
|
738
|
+
|
|
739
|
+
// Auto-hide after 5s in case stop signal is missed
|
|
740
|
+
clearTimeout(dmTypingTimer);
|
|
741
|
+
dmTypingTimer = setTimeout(function () {
|
|
742
|
+
showDmTypingIndicator(false);
|
|
743
|
+
}, 5000);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function handleDmSend() {
|
|
747
|
+
if (!dmMode || !dmKey || !inputEl) return false;
|
|
748
|
+
var text = inputEl.value.trim();
|
|
749
|
+
if (!text) return false;
|
|
750
|
+
ws.send(JSON.stringify({ type: "dm_send", dmKey: dmKey, text: text }));
|
|
751
|
+
inputEl.value = "";
|
|
752
|
+
autoResize();
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
|
|
527
756
|
var hubCloseBtn = document.getElementById("home-hub-close");
|
|
528
757
|
|
|
529
758
|
function showHomeHub() {
|
|
759
|
+
if (dmMode) exitDmMode();
|
|
530
760
|
homeHubVisible = true;
|
|
531
761
|
homeHub.classList.remove("hidden");
|
|
532
762
|
// Show close button only if there's a project to return to
|
|
@@ -611,6 +841,23 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
611
841
|
if (msg.serverUsers) {
|
|
612
842
|
renderTopbarPresence(msg.serverUsers);
|
|
613
843
|
}
|
|
844
|
+
// Update user strip (DM targets) in icon strip
|
|
845
|
+
if (msg.allUsers) {
|
|
846
|
+
cachedAllUsers = msg.allUsers;
|
|
847
|
+
var onlineIds = (msg.serverUsers || []).map(function (u) { return u.id; });
|
|
848
|
+
renderUserStrip(msg.allUsers, onlineIds, myUserId);
|
|
849
|
+
// Render my avatar (always present, hidden behind user-island)
|
|
850
|
+
var meEl = document.getElementById("icon-strip-me");
|
|
851
|
+
if (meEl && !meEl.hasChildNodes()) {
|
|
852
|
+
var myUser = cachedAllUsers.find(function (u) { return u.id === myUserId; });
|
|
853
|
+
if (myUser) {
|
|
854
|
+
var meAvatar = document.createElement("img");
|
|
855
|
+
meAvatar.className = "icon-strip-me-avatar";
|
|
856
|
+
meAvatar.src = "https://api.dicebear.com/9.x/" + (myUser.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(myUser.avatarSeed || myUser.username) + "&size=34";
|
|
857
|
+
meEl.appendChild(meAvatar);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
614
861
|
}
|
|
615
862
|
|
|
616
863
|
function renderTopbarPresence(serverUsers) {
|
|
@@ -972,6 +1219,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
972
1219
|
getUpcomingSchedules: getUpcomingSchedules,
|
|
973
1220
|
get multiUser() { return isMultiUserMode; },
|
|
974
1221
|
get myUserId() { return myUserId; },
|
|
1222
|
+
openDm: function (userId) { openDm(userId); },
|
|
975
1223
|
};
|
|
976
1224
|
initSidebar(sidebarCtx);
|
|
977
1225
|
initIconStrip(sidebarCtx);
|
|
@@ -2358,6 +2606,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
2358
2606
|
// --- Project switching (no full reload) ---
|
|
2359
2607
|
function switchProject(slug) {
|
|
2360
2608
|
if (!slug) return;
|
|
2609
|
+
if (dmMode) exitDmMode();
|
|
2361
2610
|
if (homeHubVisible) {
|
|
2362
2611
|
hideHomeHub();
|
|
2363
2612
|
if (slug === currentSlug) return;
|
|
@@ -2724,7 +2973,7 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
2724
2973
|
break;
|
|
2725
2974
|
|
|
2726
2975
|
case "input_sync":
|
|
2727
|
-
handleInputSync(msg.text);
|
|
2976
|
+
if (!dmMode) handleInputSync(msg.text);
|
|
2728
2977
|
break;
|
|
2729
2978
|
|
|
2730
2979
|
case "session_list":
|
|
@@ -3184,6 +3433,36 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
3184
3433
|
updateProjectList(msg);
|
|
3185
3434
|
break;
|
|
3186
3435
|
|
|
3436
|
+
// --- DM ---
|
|
3437
|
+
case "dm_history":
|
|
3438
|
+
enterDmMode(msg.dmKey, msg.targetUser, msg.messages);
|
|
3439
|
+
break;
|
|
3440
|
+
|
|
3441
|
+
case "dm_message":
|
|
3442
|
+
if (dmMode && msg.dmKey === dmKey) {
|
|
3443
|
+
showDmTypingIndicator(false); // hide typing when message arrives
|
|
3444
|
+
appendDmMessage(msg.message);
|
|
3445
|
+
scrollToBottom();
|
|
3446
|
+
} else if (msg.message) {
|
|
3447
|
+
// DM notification when not in that DM
|
|
3448
|
+
var fromId = msg.message.from;
|
|
3449
|
+
if (fromId && fromId !== myUserId) {
|
|
3450
|
+
dmUnread[fromId] = (dmUnread[fromId] || 0) + 1;
|
|
3451
|
+
updateDmBadge(fromId, dmUnread[fromId]);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
break;
|
|
3455
|
+
|
|
3456
|
+
case "dm_typing":
|
|
3457
|
+
if (dmMode && msg.dmKey === dmKey) {
|
|
3458
|
+
showDmTypingIndicator(msg.typing);
|
|
3459
|
+
}
|
|
3460
|
+
break;
|
|
3461
|
+
|
|
3462
|
+
case "dm_list":
|
|
3463
|
+
// Could be used for DM list view later
|
|
3464
|
+
break;
|
|
3465
|
+
|
|
3187
3466
|
case "daemon_config":
|
|
3188
3467
|
updateDaemonConfig(msg.config);
|
|
3189
3468
|
break;
|
|
@@ -3488,6 +3767,9 @@ import { initAdmin, checkAdminAccess } from './modules/admin.js';
|
|
|
3488
3767
|
showImageModal: showImageModal,
|
|
3489
3768
|
hideSuggestionChips: hideSuggestionChips,
|
|
3490
3769
|
setSendBtnMode: setSendBtnMode,
|
|
3770
|
+
isDmMode: function () { return dmMode; },
|
|
3771
|
+
getDmKey: function () { return dmKey; },
|
|
3772
|
+
handleDmSend: function () { handleDmSend(); },
|
|
3491
3773
|
});
|
|
3492
3774
|
|
|
3493
3775
|
// --- STT module (voice input via Web Speech API) ---
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
#icon-strip {
|
|
6
|
+
position: relative;
|
|
6
7
|
width: 72px;
|
|
7
8
|
flex-shrink: 0;
|
|
8
9
|
background: var(--sidebar-bg);
|
|
9
10
|
display: flex;
|
|
10
11
|
flex-direction: column;
|
|
11
12
|
align-items: center;
|
|
12
|
-
padding: 0 0
|
|
13
|
+
padding: 0 0 80px;
|
|
13
14
|
overflow: hidden;
|
|
14
15
|
z-index: 2;
|
|
15
16
|
}
|
|
@@ -114,6 +115,7 @@
|
|
|
114
115
|
/* --- Scrollable project list area --- */
|
|
115
116
|
.icon-strip-projects {
|
|
116
117
|
width: 100%;
|
|
118
|
+
min-height: 0;
|
|
117
119
|
overflow-y: auto;
|
|
118
120
|
overflow-x: hidden;
|
|
119
121
|
display: flex;
|
|
@@ -605,6 +607,165 @@
|
|
|
605
607
|
height: 22px;
|
|
606
608
|
}
|
|
607
609
|
|
|
610
|
+
/* --- User avatars (circle, above my-avatar at bottom) --- */
|
|
611
|
+
.icon-strip-users {
|
|
612
|
+
position: absolute;
|
|
613
|
+
bottom: 74px; /* above user-island (58px + 8px bottom + 8px gap) */
|
|
614
|
+
left: 50%;
|
|
615
|
+
transform: translateX(-50%);
|
|
616
|
+
width: 56px;
|
|
617
|
+
display: flex;
|
|
618
|
+
flex-direction: column;
|
|
619
|
+
align-items: center;
|
|
620
|
+
gap: 0;
|
|
621
|
+
padding: 6px 0;
|
|
622
|
+
background: var(--bg-alt);
|
|
623
|
+
border-radius: 8px;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.icon-strip-users.hidden {
|
|
627
|
+
display: none;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* "Invite" mini button at bottom of user strip */
|
|
631
|
+
.icon-strip-invite {
|
|
632
|
+
position: relative;
|
|
633
|
+
width: 40px;
|
|
634
|
+
height: 32px;
|
|
635
|
+
border-radius: 10px;
|
|
636
|
+
background: transparent;
|
|
637
|
+
border: none;
|
|
638
|
+
display: flex;
|
|
639
|
+
align-items: center;
|
|
640
|
+
justify-content: center;
|
|
641
|
+
cursor: pointer;
|
|
642
|
+
flex-shrink: 0;
|
|
643
|
+
color: var(--text-dimmer);
|
|
644
|
+
transition: color 0.2s, background 0.2s;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.icon-strip-invite:hover {
|
|
648
|
+
color: var(--accent);
|
|
649
|
+
background: rgba(var(--overlay-rgb), 0.08);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.icon-strip-invite .lucide {
|
|
653
|
+
width: 16px;
|
|
654
|
+
height: 16px;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.icon-strip-user {
|
|
658
|
+
position: relative;
|
|
659
|
+
width: 42px;
|
|
660
|
+
height: 42px;
|
|
661
|
+
border-radius: 0;
|
|
662
|
+
background: transparent;
|
|
663
|
+
display: flex;
|
|
664
|
+
align-items: center;
|
|
665
|
+
justify-content: center;
|
|
666
|
+
cursor: pointer;
|
|
667
|
+
flex-shrink: 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.icon-strip-user-avatar {
|
|
671
|
+
width: 34px;
|
|
672
|
+
height: 34px;
|
|
673
|
+
border-radius: 50%;
|
|
674
|
+
object-fit: cover;
|
|
675
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
676
|
+
opacity: 0.7;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
.icon-strip-user:hover .icon-strip-user-avatar {
|
|
680
|
+
opacity: 1;
|
|
681
|
+
transform: scale(1.08);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.icon-strip-user.active .icon-strip-user-avatar {
|
|
685
|
+
opacity: 1;
|
|
686
|
+
box-shadow: 0 0 0 2px var(--accent);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/* Online status dot (bottom-right on avatar, like project blink dot) */
|
|
690
|
+
.icon-strip-user-online {
|
|
691
|
+
position: absolute;
|
|
692
|
+
bottom: 5px;
|
|
693
|
+
right: 5px;
|
|
694
|
+
width: 10px;
|
|
695
|
+
height: 10px;
|
|
696
|
+
border-radius: 50%;
|
|
697
|
+
background: var(--text-dimmer);
|
|
698
|
+
border: 2px solid var(--sidebar-bg);
|
|
699
|
+
opacity: 1;
|
|
700
|
+
transition: opacity 0.2s, background 0.3s;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.icon-strip-user.online .icon-strip-user-online {
|
|
704
|
+
background: var(--success);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* DM unread badge */
|
|
708
|
+
.icon-strip-user-badge {
|
|
709
|
+
position: absolute;
|
|
710
|
+
top: -2px;
|
|
711
|
+
right: -2px;
|
|
712
|
+
min-width: 18px;
|
|
713
|
+
height: 18px;
|
|
714
|
+
border-radius: 9px;
|
|
715
|
+
background: #e74c3c;
|
|
716
|
+
color: #fff;
|
|
717
|
+
font-size: 11px;
|
|
718
|
+
font-weight: 700;
|
|
719
|
+
display: none;
|
|
720
|
+
align-items: center;
|
|
721
|
+
justify-content: center;
|
|
722
|
+
padding: 0 5px;
|
|
723
|
+
line-height: 18px;
|
|
724
|
+
text-align: center;
|
|
725
|
+
box-shadow: 0 0 0 2px var(--bg);
|
|
726
|
+
z-index: 2;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.icon-strip-user-badge.has-unread {
|
|
730
|
+
display: flex;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/* Pill for user icons */
|
|
734
|
+
.icon-strip-user:hover .icon-strip-pill {
|
|
735
|
+
opacity: 1;
|
|
736
|
+
height: 20px;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.icon-strip-user.active .icon-strip-pill {
|
|
740
|
+
opacity: 1;
|
|
741
|
+
height: 32px;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/* DM mode: keep same position (user-island gone but layout stays) */
|
|
745
|
+
|
|
746
|
+
/* --- My avatar at bottom of icon strip (always present, behind user-island) --- */
|
|
747
|
+
/* Positioned to align exactly with user-island avatar (left:8px + padding:6px + 18px center = 32px from left edge of layout-body) */
|
|
748
|
+
/* icon-strip is 72px wide, so 32px from left = left offset within icon-strip */
|
|
749
|
+
.icon-strip-me {
|
|
750
|
+
position: absolute;
|
|
751
|
+
bottom: 19px; /* vertically center with user-island (bottom:8px + height:58px/2 - 16px) */
|
|
752
|
+
left: 32px;
|
|
753
|
+
transform: translateX(-50%);
|
|
754
|
+
display: flex;
|
|
755
|
+
align-items: center;
|
|
756
|
+
justify-content: center;
|
|
757
|
+
width: 32px;
|
|
758
|
+
height: 32px;
|
|
759
|
+
z-index: 1; /* below user-island z-index: 15 */
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.icon-strip-me-avatar {
|
|
763
|
+
width: 32px;
|
|
764
|
+
height: 32px;
|
|
765
|
+
border-radius: 50%;
|
|
766
|
+
object-fit: cover;
|
|
767
|
+
}
|
|
768
|
+
|
|
608
769
|
/* --- Per-project presence avatars --- */
|
|
609
770
|
/* --- Mobile: hide icon strip --- */
|
|
610
771
|
@media (max-width: 768px) {
|
|
@@ -1465,3 +1465,177 @@ pre.mermaid-error {
|
|
|
1465
1465
|
|
|
1466
1466
|
/* Rate limit inline cards removed — now header-only with popover (see title-bar.css) */
|
|
1467
1467
|
|
|
1468
|
+
/* ==========================================================================
|
|
1469
|
+
DM Messages (P2P chat)
|
|
1470
|
+
========================================================================== */
|
|
1471
|
+
|
|
1472
|
+
/* --- Slack-style DM messages --- */
|
|
1473
|
+
|
|
1474
|
+
.dm-msg {
|
|
1475
|
+
display: flex;
|
|
1476
|
+
align-items: flex-start;
|
|
1477
|
+
gap: 8px;
|
|
1478
|
+
padding: 4px 16px;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
.dm-msg:hover {
|
|
1482
|
+
background: var(--bg-alt);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.dm-msg-avatar {
|
|
1486
|
+
width: 36px;
|
|
1487
|
+
height: 36px;
|
|
1488
|
+
border-radius: 6px;
|
|
1489
|
+
flex-shrink: 0;
|
|
1490
|
+
margin-top: 2px;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.dm-msg-content {
|
|
1494
|
+
flex: 1;
|
|
1495
|
+
min-width: 0;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.dm-msg-header {
|
|
1499
|
+
display: flex;
|
|
1500
|
+
align-items: baseline;
|
|
1501
|
+
gap: 8px;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.dm-msg-name {
|
|
1505
|
+
font-weight: 700;
|
|
1506
|
+
font-size: 15px;
|
|
1507
|
+
color: var(--text);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
.dm-msg-time {
|
|
1511
|
+
font-size: 12px;
|
|
1512
|
+
color: var(--text-dimmer);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.dm-msg-body {
|
|
1516
|
+
font-size: 15px;
|
|
1517
|
+
line-height: 1.46;
|
|
1518
|
+
color: var(--text);
|
|
1519
|
+
word-wrap: break-word;
|
|
1520
|
+
white-space: pre-wrap;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/* Compact: same sender within 5 min */
|
|
1524
|
+
.dm-msg-compact {
|
|
1525
|
+
padding: 1px 16px 1px 60px; /* 16 + 36 avatar + 8 gap = 60 */
|
|
1526
|
+
position: relative;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
.dm-msg-compact:hover .dm-msg-hover-time {
|
|
1530
|
+
opacity: 1;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
.dm-msg-hover-time {
|
|
1534
|
+
position: absolute;
|
|
1535
|
+
left: 16px;
|
|
1536
|
+
width: 36px;
|
|
1537
|
+
text-align: center;
|
|
1538
|
+
font-size: 11px;
|
|
1539
|
+
color: var(--text-dimmer);
|
|
1540
|
+
opacity: 0;
|
|
1541
|
+
transition: opacity 0.1s;
|
|
1542
|
+
line-height: 1.46;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/* Typing indicator */
|
|
1546
|
+
.dm-typing-indicator {
|
|
1547
|
+
align-items: center;
|
|
1548
|
+
gap: 8px;
|
|
1549
|
+
padding: 4px 16px;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.dm-typing-indicator .dm-msg-avatar {
|
|
1553
|
+
opacity: 0.7;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.dm-typing-dots {
|
|
1557
|
+
display: flex;
|
|
1558
|
+
align-items: center;
|
|
1559
|
+
gap: 4px;
|
|
1560
|
+
padding: 8px 12px;
|
|
1561
|
+
background: var(--bg-alt);
|
|
1562
|
+
border-radius: 12px;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.dm-typing-dots span {
|
|
1566
|
+
width: 7px;
|
|
1567
|
+
height: 7px;
|
|
1568
|
+
border-radius: 50%;
|
|
1569
|
+
background: var(--text-dimmer);
|
|
1570
|
+
animation: dm-typing-bounce 1.4s infinite ease-in-out both;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.dm-typing-dots span:nth-child(1) { animation-delay: 0s; }
|
|
1574
|
+
.dm-typing-dots span:nth-child(2) { animation-delay: 0.2s; }
|
|
1575
|
+
.dm-typing-dots span:nth-child(3) { animation-delay: 0.4s; }
|
|
1576
|
+
|
|
1577
|
+
@keyframes dm-typing-bounce {
|
|
1578
|
+
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
|
1579
|
+
40% { transform: scale(1); opacity: 1; }
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
/* DM mode: hide project-specific UI elements */
|
|
1583
|
+
#main-column.dm-mode .msg-group,
|
|
1584
|
+
#main-column.dm-mode #suggestion-chips,
|
|
1585
|
+
#main-column.dm-mode .tool-group {
|
|
1586
|
+
display: none;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/* DM mode: simplify input bar (no voice, no model config, no context, no slash menu) */
|
|
1590
|
+
#main-column.dm-mode #stt-btn,
|
|
1591
|
+
#main-column.dm-mode #config-chip-wrap,
|
|
1592
|
+
#main-column.dm-mode #context-mini,
|
|
1593
|
+
#main-column.dm-mode #slash-menu {
|
|
1594
|
+
display: none !important;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/* DM mode: hide sidebar column and resize handle */
|
|
1598
|
+
#sidebar-column.dm-mode {
|
|
1599
|
+
display: none !important;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
#sidebar-resize-handle.dm-mode {
|
|
1603
|
+
display: none !important;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/* DM header bar: replaces normal title-bar-content */
|
|
1607
|
+
.dm-header-bar {
|
|
1608
|
+
display: none;
|
|
1609
|
+
align-items: center;
|
|
1610
|
+
gap: 12px;
|
|
1611
|
+
padding: 0 16px;
|
|
1612
|
+
height: 48px;
|
|
1613
|
+
flex-shrink: 0;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
#main-column.dm-mode .title-bar-content {
|
|
1617
|
+
display: none;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
#main-column.dm-mode .dm-header-bar {
|
|
1621
|
+
display: flex;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
.dm-header-avatar {
|
|
1625
|
+
width: 28px;
|
|
1626
|
+
height: 28px;
|
|
1627
|
+
border-radius: 50%;
|
|
1628
|
+
object-fit: cover;
|
|
1629
|
+
flex-shrink: 0;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
.dm-header-name {
|
|
1633
|
+
font-size: 15px;
|
|
1634
|
+
font-weight: 700;
|
|
1635
|
+
color: #fff;
|
|
1636
|
+
white-space: nowrap;
|
|
1637
|
+
overflow: hidden;
|
|
1638
|
+
text-overflow: ellipsis;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
|
package/lib/public/index.html
CHANGED
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
<div class="icon-strip-separator"></div>
|
|
69
69
|
<div class="icon-strip-projects" id="icon-strip-projects"></div>
|
|
70
70
|
<button class="icon-strip-add" id="icon-strip-add" title="Add project"><i data-lucide="plus"></i></button>
|
|
71
|
+
<div class="icon-strip-users hidden" id="icon-strip-users"></div>
|
|
72
|
+
<div class="icon-strip-me" id="icon-strip-me"></div>
|
|
71
73
|
</div>
|
|
72
74
|
|
|
73
75
|
<!-- === Main Area (sidebar + resize-handle + main-column) === -->
|
|
@@ -191,6 +193,10 @@
|
|
|
191
193
|
<div id="sidebar-resize-handle"></div>
|
|
192
194
|
<div id="sidebar-overlay"></div>
|
|
193
195
|
<div id="main-column">
|
|
196
|
+
<div class="dm-header-bar" id="dm-header-bar">
|
|
197
|
+
<img id="dm-header-avatar" class="dm-header-avatar" alt="">
|
|
198
|
+
<span id="dm-header-name" class="dm-header-name"></span>
|
|
199
|
+
</div>
|
|
194
200
|
<div class="title-bar-content">
|
|
195
201
|
<div id="header-left">
|
|
196
202
|
<button id="sidebar-expand-btn" title="Open sidebar"><i data-lucide="panel-left-open"></i></button>
|
|
@@ -23,6 +23,11 @@ export var builtinCommands = [
|
|
|
23
23
|
|
|
24
24
|
// --- Send ---
|
|
25
25
|
export function sendMessage() {
|
|
26
|
+
// DM mode intercept: if in DM mode, route to DM handler instead
|
|
27
|
+
if (ctx.isDmMode && ctx.isDmMode() && ctx.handleDmSend) {
|
|
28
|
+
ctx.handleDmSend();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
26
31
|
var text = ctx.inputEl.value.trim();
|
|
27
32
|
var images = pendingImages.slice();
|
|
28
33
|
if (!text && images.length === 0 && pendingPastes.length === 0 && pendingFiles.length === 0) return;
|
|
@@ -443,9 +448,15 @@ function updateSlashHighlight() {
|
|
|
443
448
|
// --- Input sync across devices ---
|
|
444
449
|
function sendInputSync() {
|
|
445
450
|
if (isRemoteInput) return;
|
|
446
|
-
if (ctx.ws
|
|
447
|
-
|
|
451
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
452
|
+
// In DM mode, send typing indicator instead of input_sync
|
|
453
|
+
if (ctx.isDmMode && ctx.isDmMode()) {
|
|
454
|
+
var hasText = ctx.inputEl.value.length > 0;
|
|
455
|
+
var dk = ctx.getDmKey ? ctx.getDmKey() : null;
|
|
456
|
+
if (dk) ctx.ws.send(JSON.stringify({ type: "dm_typing", dmKey: dk, typing: hasText }));
|
|
457
|
+
return;
|
|
448
458
|
}
|
|
459
|
+
ctx.ws.send(JSON.stringify({ type: "input_sync", text: ctx.inputEl.value }));
|
|
449
460
|
}
|
|
450
461
|
|
|
451
462
|
export function handleInputSync(text) {
|
|
@@ -2087,7 +2087,8 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2087
2087
|
for (var i = 0; i < projects.length; i++) {
|
|
2088
2088
|
var p = projects[i];
|
|
2089
2089
|
var el = document.createElement("a");
|
|
2090
|
-
|
|
2090
|
+
var isActive = p.slug === currentSlug && !currentDmUserId;
|
|
2091
|
+
el.className = "icon-strip-item" + (isActive ? " active" : "");
|
|
2091
2092
|
el.href = "/p/" + p.slug + "/";
|
|
2092
2093
|
el.dataset.slug = p.slug;
|
|
2093
2094
|
|
|
@@ -2144,7 +2145,7 @@ export function renderIconStrip(projects, currentSlug) {
|
|
|
2144
2145
|
// Update home icon active state
|
|
2145
2146
|
var homeIcon = document.querySelector(".icon-strip-home");
|
|
2146
2147
|
if (homeIcon) {
|
|
2147
|
-
if (!currentSlug || projects.length === 0) {
|
|
2148
|
+
if ((!currentSlug || projects.length === 0) && !currentDmUserId) {
|
|
2148
2149
|
homeIcon.classList.add("active");
|
|
2149
2150
|
} else {
|
|
2150
2151
|
homeIcon.classList.remove("active");
|
|
@@ -2193,11 +2194,112 @@ function renderProjectList(projects, currentSlug) {
|
|
|
2193
2194
|
|
|
2194
2195
|
export function getEmojiCategories() { return EMOJI_CATEGORIES; }
|
|
2195
2196
|
|
|
2197
|
+
// --- User strip (DM targets) ---
|
|
2198
|
+
var cachedAllUsers = [];
|
|
2199
|
+
var cachedOnlineUserIds = [];
|
|
2200
|
+
var currentDmUserId = null;
|
|
2201
|
+
|
|
2202
|
+
export function renderUserStrip(allUsers, onlineUserIds, myUserId) {
|
|
2203
|
+
cachedAllUsers = allUsers || [];
|
|
2204
|
+
cachedOnlineUserIds = onlineUserIds || [];
|
|
2205
|
+
var container = document.getElementById("icon-strip-users");
|
|
2206
|
+
if (!container) return;
|
|
2207
|
+
|
|
2208
|
+
// Filter out self, only show other users
|
|
2209
|
+
var others = cachedAllUsers.filter(function (u) { return u.id !== myUserId; });
|
|
2210
|
+
|
|
2211
|
+
// Hide section if no other users (single-user mode or alone)
|
|
2212
|
+
if (others.length === 0) {
|
|
2213
|
+
container.innerHTML = "";
|
|
2214
|
+
container.classList.add("hidden");
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
container.classList.remove("hidden");
|
|
2219
|
+
container.innerHTML = "";
|
|
2220
|
+
|
|
2221
|
+
for (var i = 0; i < others.length; i++) {
|
|
2222
|
+
(function (u) {
|
|
2223
|
+
var el = document.createElement("div");
|
|
2224
|
+
el.className = "icon-strip-user";
|
|
2225
|
+
el.dataset.userId = u.id;
|
|
2226
|
+
if (u.id === currentDmUserId) el.classList.add("active");
|
|
2227
|
+
if (onlineUserIds.indexOf(u.id) !== -1) el.classList.add("online");
|
|
2228
|
+
|
|
2229
|
+
var pill = document.createElement("span");
|
|
2230
|
+
pill.className = "icon-strip-pill";
|
|
2231
|
+
el.appendChild(pill);
|
|
2232
|
+
|
|
2233
|
+
var avatar = document.createElement("img");
|
|
2234
|
+
avatar.className = "icon-strip-user-avatar";
|
|
2235
|
+
avatar.src = "https://api.dicebear.com/9.x/" + (u.avatarStyle || "thumbs") + "/svg?seed=" + encodeURIComponent(u.avatarSeed || u.username) + "&size=34";
|
|
2236
|
+
avatar.alt = u.displayName;
|
|
2237
|
+
el.appendChild(avatar);
|
|
2238
|
+
|
|
2239
|
+
var onlineDot = document.createElement("span");
|
|
2240
|
+
onlineDot.className = "icon-strip-user-online";
|
|
2241
|
+
el.appendChild(onlineDot);
|
|
2242
|
+
|
|
2243
|
+
var badge = document.createElement("span");
|
|
2244
|
+
badge.className = "icon-strip-user-badge";
|
|
2245
|
+
badge.dataset.userId = u.id;
|
|
2246
|
+
el.appendChild(badge);
|
|
2247
|
+
|
|
2248
|
+
// Tooltip
|
|
2249
|
+
el.addEventListener("mouseenter", function () { showIconTooltip(el, u.displayName); });
|
|
2250
|
+
el.addEventListener("mouseleave", hideIconTooltip);
|
|
2251
|
+
|
|
2252
|
+
// Click: open DM
|
|
2253
|
+
el.addEventListener("click", function () {
|
|
2254
|
+
if (ctx.openDm) ctx.openDm(u.id);
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
container.appendChild(el);
|
|
2258
|
+
})(others[i]);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// Invite button at bottom of user strip
|
|
2262
|
+
var inviteBtn = document.createElement("button");
|
|
2263
|
+
inviteBtn.className = "icon-strip-invite";
|
|
2264
|
+
inviteBtn.innerHTML = iconHtml("user-plus");
|
|
2265
|
+
inviteBtn.addEventListener("click", function () { triggerShare(); });
|
|
2266
|
+
inviteBtn.addEventListener("mouseenter", function () { showIconTooltip(inviteBtn, "Invite"); });
|
|
2267
|
+
inviteBtn.addEventListener("mouseleave", hideIconTooltip);
|
|
2268
|
+
container.appendChild(inviteBtn);
|
|
2269
|
+
refreshIcons();
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
export function setCurrentDmUser(userId) {
|
|
2273
|
+
currentDmUserId = userId;
|
|
2274
|
+
// Update active state on user icons immediately
|
|
2275
|
+
var container = document.getElementById("icon-strip-users");
|
|
2276
|
+
if (!container) return;
|
|
2277
|
+
var items = container.querySelectorAll(".icon-strip-user");
|
|
2278
|
+
for (var i = 0; i < items.length; i++) {
|
|
2279
|
+
if (items[i].dataset.userId === userId) {
|
|
2280
|
+
items[i].classList.add("active");
|
|
2281
|
+
} else {
|
|
2282
|
+
items[i].classList.remove("active");
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
export function updateDmBadge(userId, count) {
|
|
2288
|
+
var badge = document.querySelector('.icon-strip-user-badge[data-user-id="' + userId + '"]');
|
|
2289
|
+
if (!badge) return;
|
|
2290
|
+
if (count > 0) {
|
|
2291
|
+
badge.textContent = count > 99 ? "99+" : String(count);
|
|
2292
|
+
badge.classList.add("has-unread");
|
|
2293
|
+
} else {
|
|
2294
|
+
badge.textContent = "";
|
|
2295
|
+
badge.classList.remove("has-unread");
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2196
2299
|
export function initIconStrip(_ctx) {
|
|
2197
2300
|
var addBtn = document.getElementById("icon-strip-add");
|
|
2198
2301
|
if (addBtn) {
|
|
2199
2302
|
addBtn.addEventListener("click", function () {
|
|
2200
|
-
// Reuse existing add-project modal
|
|
2201
2303
|
var modal = _ctx.$("add-project-modal");
|
|
2202
2304
|
if (modal) modal.classList.remove("hidden");
|
|
2203
2305
|
});
|
package/lib/server.js
CHANGED
|
@@ -7,6 +7,7 @@ var { pinPageHtml, setupPageHtml, adminSetupPageHtml, multiUserLoginPageHtml, sm
|
|
|
7
7
|
var smtp = require("./smtp");
|
|
8
8
|
var { createProjectContext } = require("./project");
|
|
9
9
|
var users = require("./users");
|
|
10
|
+
var dm = require("./dm");
|
|
10
11
|
|
|
11
12
|
var { CONFIG_DIR } = require("./config");
|
|
12
13
|
|
|
@@ -1894,12 +1895,100 @@ function createServer(opts) {
|
|
|
1894
1895
|
onSetPin: onSetPin,
|
|
1895
1896
|
onSetKeepAwake: onSetKeepAwake,
|
|
1896
1897
|
onShutdown: onShutdown,
|
|
1898
|
+
onDmMessage: handleDmMessage,
|
|
1897
1899
|
});
|
|
1898
1900
|
projects.set(slug, ctx);
|
|
1899
1901
|
ctx.warmup();
|
|
1900
1902
|
return true;
|
|
1901
1903
|
}
|
|
1902
1904
|
|
|
1905
|
+
// --- DM message handler (server-level, cross-project) ---
|
|
1906
|
+
function handleDmMessage(ws, msg) {
|
|
1907
|
+
if (!users.isMultiUser() || !ws._clayUser) return;
|
|
1908
|
+
var userId = ws._clayUser.id;
|
|
1909
|
+
|
|
1910
|
+
if (msg.type === "dm_list") {
|
|
1911
|
+
var dmList = dm.getDmList(userId);
|
|
1912
|
+
// Enrich with user info
|
|
1913
|
+
for (var i = 0; i < dmList.length; i++) {
|
|
1914
|
+
var otherUser = users.findUserById(dmList[i].otherUserId);
|
|
1915
|
+
if (otherUser) {
|
|
1916
|
+
var p = otherUser.profile || {};
|
|
1917
|
+
dmList[i].otherUser = {
|
|
1918
|
+
id: otherUser.id,
|
|
1919
|
+
displayName: p.name || otherUser.displayName || otherUser.username,
|
|
1920
|
+
username: otherUser.username,
|
|
1921
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
1922
|
+
avatarSeed: p.avatarSeed || otherUser.username,
|
|
1923
|
+
avatarColor: p.avatarColor || "#7c3aed",
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
ws.send(JSON.stringify({ type: "dm_list", dms: dmList }));
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
if (msg.type === "dm_open") {
|
|
1932
|
+
if (!msg.targetUserId) return;
|
|
1933
|
+
var result = dm.openDm(userId, msg.targetUserId);
|
|
1934
|
+
var targetUser = users.findUserById(msg.targetUserId);
|
|
1935
|
+
var tp = targetUser ? (targetUser.profile || {}) : {};
|
|
1936
|
+
ws.send(JSON.stringify({
|
|
1937
|
+
type: "dm_history",
|
|
1938
|
+
dmKey: result.dmKey,
|
|
1939
|
+
messages: result.messages,
|
|
1940
|
+
targetUser: targetUser ? {
|
|
1941
|
+
id: targetUser.id,
|
|
1942
|
+
displayName: tp.name || targetUser.displayName || targetUser.username,
|
|
1943
|
+
username: targetUser.username,
|
|
1944
|
+
avatarStyle: tp.avatarStyle || "thumbs",
|
|
1945
|
+
avatarSeed: tp.avatarSeed || targetUser.username,
|
|
1946
|
+
avatarColor: tp.avatarColor || "#7c3aed",
|
|
1947
|
+
} : null,
|
|
1948
|
+
}));
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
if (msg.type === "dm_typing") {
|
|
1953
|
+
// Relay typing indicator to DM partner
|
|
1954
|
+
var dmKey = msg.dmKey;
|
|
1955
|
+
if (!dmKey) return;
|
|
1956
|
+
var parts = dmKey.split(":");
|
|
1957
|
+
if (parts.indexOf(userId) === -1) return;
|
|
1958
|
+
var targetId = parts[0] === userId ? parts[1] : parts[0];
|
|
1959
|
+
projects.forEach(function (ctx) {
|
|
1960
|
+
ctx.forEachClient(function (otherWs) {
|
|
1961
|
+
if (otherWs === ws) return;
|
|
1962
|
+
if (!otherWs._clayUser || otherWs._clayUser.id !== targetId) return;
|
|
1963
|
+
if (otherWs.readyState !== 1) return;
|
|
1964
|
+
otherWs.send(JSON.stringify({ type: "dm_typing", dmKey: dmKey, userId: userId, typing: !!msg.typing }));
|
|
1965
|
+
});
|
|
1966
|
+
});
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
if (msg.type === "dm_send") {
|
|
1971
|
+
if (!msg.dmKey || !msg.text) return;
|
|
1972
|
+
// Verify sender is a participant
|
|
1973
|
+
var parts = msg.dmKey.split(":");
|
|
1974
|
+
if (parts.indexOf(userId) === -1) return;
|
|
1975
|
+
var message = dm.sendMessage(msg.dmKey, userId, msg.text);
|
|
1976
|
+
// Send confirmation to sender
|
|
1977
|
+
ws.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
|
|
1978
|
+
// Broadcast to target user's connections across all projects
|
|
1979
|
+
var targetId = parts[0] === userId ? parts[1] : parts[0];
|
|
1980
|
+
projects.forEach(function (ctx) {
|
|
1981
|
+
ctx.forEachClient(function (otherWs) {
|
|
1982
|
+
if (otherWs === ws) return;
|
|
1983
|
+
if (!otherWs._clayUser || otherWs._clayUser.id !== targetId) return;
|
|
1984
|
+
if (otherWs.readyState !== 1) return;
|
|
1985
|
+
otherWs.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1903
1992
|
function removeProject(slug) {
|
|
1904
1993
|
var ctx = projects.get(slug);
|
|
1905
1994
|
if (!ctx) return false;
|
|
@@ -1989,6 +2078,18 @@ function createServer(opts) {
|
|
|
1989
2078
|
return;
|
|
1990
2079
|
}
|
|
1991
2080
|
var serverUsers = getServerUsers();
|
|
2081
|
+
var allUsers = users.getAllUsers().map(function (u) {
|
|
2082
|
+
var p = u.profile || {};
|
|
2083
|
+
return {
|
|
2084
|
+
id: u.id,
|
|
2085
|
+
displayName: p.name || u.displayName || u.username,
|
|
2086
|
+
username: u.username,
|
|
2087
|
+
role: u.role,
|
|
2088
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
2089
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
2090
|
+
avatarColor: p.avatarColor || "#7c3aed",
|
|
2091
|
+
};
|
|
2092
|
+
});
|
|
1992
2093
|
// Build per-user filtered lists, send individually
|
|
1993
2094
|
var sentUsers = {};
|
|
1994
2095
|
projects.forEach(function (ctx) {
|
|
@@ -2014,6 +2115,7 @@ function createServer(opts) {
|
|
|
2014
2115
|
projects: filteredProjects,
|
|
2015
2116
|
projectCount: projects.size,
|
|
2016
2117
|
serverUsers: serverUsers,
|
|
2118
|
+
allUsers: allUsers,
|
|
2017
2119
|
});
|
|
2018
2120
|
sentUsers[key] = msgStr;
|
|
2019
2121
|
ws.send(msgStr);
|
package/lib/users.js
CHANGED
|
@@ -223,6 +223,12 @@ function getAllUsers() {
|
|
|
223
223
|
});
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
function getOtherUsers(excludeUserId) {
|
|
227
|
+
return getAllUsers().filter(function (u) {
|
|
228
|
+
return u.id !== excludeUserId;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
226
232
|
function removeUser(userId) {
|
|
227
233
|
var data = loadUsers();
|
|
228
234
|
var before = data.users.length;
|
|
@@ -456,4 +462,5 @@ module.exports = {
|
|
|
456
462
|
canAccessProject: canAccessProject,
|
|
457
463
|
getAccessibleProjects: getAccessibleProjects,
|
|
458
464
|
canAccessSession: canAccessSession,
|
|
465
|
+
getOtherUsers: getOtherUsers,
|
|
459
466
|
};
|