clay-server 4.6.1-beta.1 → 4.7.0-beta.2
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/project-ask-user.js +6 -0
- package/lib/project-connection.js +0 -14
- package/lib/project-sessions.js +0 -18
- package/lib/project-worker-permission.js +12 -8
- package/lib/project-worker-question.js +125 -0
- package/lib/project.js +4 -1
- package/lib/public/app.js +14 -14
- package/lib/public/css/pane.css +0 -1
- package/lib/public/css/projects-hub.css +75 -0
- package/lib/public/css/tui-attention.css +0 -407
- package/lib/public/index.html +3 -14
- package/lib/public/modules/app-home-hub.js +8 -3
- package/lib/public/modules/app-messages.js +0 -15
- package/lib/public/modules/app-projects.js +16 -11
- package/lib/public/modules/home-surface-boot.js +10 -3
- package/lib/public/modules/home-surface.js +13 -1
- package/lib/public/modules/projects-hub-view.js +62 -0
- package/lib/public/modules/projects-hub.js +105 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +2 -0
- package/lib/server.js +1 -1
- package/lib/users-home-surface-preferences.js +4 -2
- package/lib/users-preferences.js +0 -36
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +0 -3
- package/package.json +1 -1
- package/lib/public/modules/whats-new-article.js +0 -70
- package/lib/public/modules/whats-new.js +0 -264
- package/lib/whats-new-content.js +0 -84
- package/lib/whats-new.js +0 -57
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
// What's New viewer.
|
|
2
|
-
//
|
|
3
|
-
// Content-agnostic carousel popup. The server pushes an array of unseen
|
|
4
|
-
// entries on connect (`whats_new_state`); this module renders them as a
|
|
5
|
-
// single carousel that the user flips through with prev/next chevrons or
|
|
6
|
-
// dot indicators. Each card shows image + title + short summary + a
|
|
7
|
-
// "Read more" button that jumps to the home page's What's New feed for
|
|
8
|
-
// the detail view.
|
|
9
|
-
//
|
|
10
|
-
// Dismiss behavior:
|
|
11
|
-
// - X / Esc / backdrop click closes the carousel and marks ALL queued
|
|
12
|
-
// entries as seen on the server (one whats_new_seen per id).
|
|
13
|
-
// - "Read more" marks the current entry seen and opens the home page
|
|
14
|
-
// anchored on that entry.
|
|
15
|
-
//
|
|
16
|
-
// Adding a new entry is a server-side content change only - see
|
|
17
|
-
// lib/whats-new-content.js. This module should not need to be edited.
|
|
18
|
-
|
|
19
|
-
import { escapeHtml } from './utils.js';
|
|
20
|
-
import { getWs } from './ws-ref.js';
|
|
21
|
-
|
|
22
|
-
var entries = []; // current carousel batch
|
|
23
|
-
var currentIndex = 0;
|
|
24
|
-
var rootEl = null;
|
|
25
|
-
var dismissedThisSession = {}; // ids we've already marked seen this session
|
|
26
|
-
var initialized = false;
|
|
27
|
-
var keyHandlerBound = false;
|
|
28
|
-
var onReadMore = null; // injected by initWhatsNew
|
|
29
|
-
|
|
30
|
-
export function initWhatsNew(opts) {
|
|
31
|
-
if (initialized) return;
|
|
32
|
-
initialized = true;
|
|
33
|
-
if (opts && typeof opts.onReadMore === "function") onReadMore = opts.onReadMore;
|
|
34
|
-
if (!keyHandlerBound) {
|
|
35
|
-
document.addEventListener("keydown", onKeyDown);
|
|
36
|
-
keyHandlerBound = true;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function handleWhatsNewState(msg) {
|
|
41
|
-
if (!msg || !Array.isArray(msg.entries) || msg.entries.length === 0) return;
|
|
42
|
-
// The server sends the full entries list plus the subset of ids the
|
|
43
|
-
// user hasn't dismissed yet. The home feed uses the full list (via
|
|
44
|
-
// setKnownEntries in app-messages); we only auto-pop the carousel for
|
|
45
|
-
// unseen ids.
|
|
46
|
-
var unseenIds = Array.isArray(msg.unseenIds) ? msg.unseenIds : msg.entries.map(function (e) { return e && e.id; });
|
|
47
|
-
var unseenSet = {};
|
|
48
|
-
for (var u = 0; u < unseenIds.length; u++) unseenSet[unseenIds[u]] = true;
|
|
49
|
-
|
|
50
|
-
var fresh = [];
|
|
51
|
-
for (var i = 0; i < msg.entries.length; i++) {
|
|
52
|
-
var e = msg.entries[i];
|
|
53
|
-
if (!e || !e.id) continue;
|
|
54
|
-
if (!unseenSet[e.id]) continue;
|
|
55
|
-
if (dismissedThisSession[e.id]) continue;
|
|
56
|
-
fresh.push(e);
|
|
57
|
-
}
|
|
58
|
-
if (fresh.length === 0) return;
|
|
59
|
-
if (rootEl) {
|
|
60
|
-
entries = entries.concat(fresh);
|
|
61
|
-
renderCarousel();
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
entries = fresh;
|
|
65
|
-
currentIndex = 0;
|
|
66
|
-
openCarousel();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function handleWhatsNewSeenResult(msg) {
|
|
70
|
-
if (msg && msg.ok === false && window && window.console) {
|
|
71
|
-
console.warn("[whats-new] mark seen failed:", msg.error);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Returns the most recent batch of entries (the same array the server
|
|
76
|
-
// last sent us). The home feed reads from this. Empty until the server
|
|
77
|
-
// has pushed at least once during this session.
|
|
78
|
-
var allKnownEntries = [];
|
|
79
|
-
export function getKnownEntries() {
|
|
80
|
-
return allKnownEntries.slice();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function setKnownEntries(list) {
|
|
84
|
-
// Used by app-messages on whats_new_state to keep the home feed in sync
|
|
85
|
-
// even after dismiss empties the carousel array.
|
|
86
|
-
if (!Array.isArray(list)) return;
|
|
87
|
-
// Merge by id, preserving order from the new list.
|
|
88
|
-
var seen = {};
|
|
89
|
-
var merged = [];
|
|
90
|
-
for (var i = 0; i < list.length; i++) {
|
|
91
|
-
if (list[i] && list[i].id && !seen[list[i].id]) {
|
|
92
|
-
seen[list[i].id] = true;
|
|
93
|
-
merged.push(list[i]);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
for (var j = 0; j < allKnownEntries.length; j++) {
|
|
97
|
-
var k = allKnownEntries[j];
|
|
98
|
-
if (k && k.id && !seen[k.id]) {
|
|
99
|
-
seen[k.id] = true;
|
|
100
|
-
merged.push(k);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
allKnownEntries = merged;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ----------------------------------------------------------------------
|
|
107
|
-
// Render
|
|
108
|
-
// ----------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
function openCarousel() {
|
|
111
|
-
rootEl = document.createElement("div");
|
|
112
|
-
rootEl.className = "whats-new-backdrop";
|
|
113
|
-
rootEl.innerHTML =
|
|
114
|
-
'<div class="whats-new-card" role="dialog" aria-modal="true">' +
|
|
115
|
-
'<button type="button" class="whats-new-close" aria-label="Close">' +
|
|
116
|
-
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' +
|
|
117
|
-
'</button>' +
|
|
118
|
-
'<div class="whats-new-image-slot"></div>' +
|
|
119
|
-
'<div class="whats-new-body">' +
|
|
120
|
-
'<div class="whats-new-eyebrow">What\'s new</div>' +
|
|
121
|
-
'<h2 class="whats-new-title"></h2>' +
|
|
122
|
-
'<p class="whats-new-summary"></p>' +
|
|
123
|
-
'<button type="button" class="whats-new-read-more">' +
|
|
124
|
-
'Read more' +
|
|
125
|
-
' <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: -2px; margin-left: 2px;"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>' +
|
|
126
|
-
'</button>' +
|
|
127
|
-
'</div>' +
|
|
128
|
-
'<div class="whats-new-nav">' +
|
|
129
|
-
'<button type="button" class="whats-new-prev" aria-label="Previous">' +
|
|
130
|
-
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>' +
|
|
131
|
-
'</button>' +
|
|
132
|
-
'<div class="whats-new-dots"></div>' +
|
|
133
|
-
'<button type="button" class="whats-new-next" aria-label="Next">' +
|
|
134
|
-
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>' +
|
|
135
|
-
'</button>' +
|
|
136
|
-
'</div>' +
|
|
137
|
-
'</div>';
|
|
138
|
-
document.body.appendChild(rootEl);
|
|
139
|
-
|
|
140
|
-
rootEl.querySelector(".whats-new-close").addEventListener("click", function (ev) {
|
|
141
|
-
ev.stopPropagation();
|
|
142
|
-
closeAndMarkSeen();
|
|
143
|
-
});
|
|
144
|
-
rootEl.querySelector(".whats-new-prev").addEventListener("click", function (ev) {
|
|
145
|
-
ev.stopPropagation();
|
|
146
|
-
go(-1);
|
|
147
|
-
});
|
|
148
|
-
rootEl.querySelector(".whats-new-next").addEventListener("click", function (ev) {
|
|
149
|
-
ev.stopPropagation();
|
|
150
|
-
go(1);
|
|
151
|
-
});
|
|
152
|
-
rootEl.querySelector(".whats-new-read-more").addEventListener("click", function (ev) {
|
|
153
|
-
ev.stopPropagation();
|
|
154
|
-
readMoreCurrent();
|
|
155
|
-
});
|
|
156
|
-
rootEl.addEventListener("click", function (ev) {
|
|
157
|
-
if (ev.target === rootEl) closeAndMarkSeen();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
renderCarousel();
|
|
161
|
-
requestAnimationFrame(function () {
|
|
162
|
-
if (rootEl) rootEl.classList.add("show");
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function renderCarousel() {
|
|
167
|
-
if (!rootEl || entries.length === 0) return;
|
|
168
|
-
if (currentIndex < 0) currentIndex = 0;
|
|
169
|
-
if (currentIndex >= entries.length) currentIndex = entries.length - 1;
|
|
170
|
-
var entry = entries[currentIndex];
|
|
171
|
-
|
|
172
|
-
var imgSlot = rootEl.querySelector(".whats-new-image-slot");
|
|
173
|
-
if (entry.image) {
|
|
174
|
-
imgSlot.innerHTML = '<img src="' + escapeHtml(entry.image) + '" alt="">';
|
|
175
|
-
imgSlot.classList.remove("empty");
|
|
176
|
-
} else {
|
|
177
|
-
imgSlot.innerHTML = '';
|
|
178
|
-
imgSlot.classList.add("empty");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
rootEl.querySelector(".whats-new-title").textContent = entry.title || "What's new";
|
|
182
|
-
rootEl.querySelector(".whats-new-summary").textContent = entry.summary || "";
|
|
183
|
-
|
|
184
|
-
var dotsEl = rootEl.querySelector(".whats-new-dots");
|
|
185
|
-
var navEl = rootEl.querySelector(".whats-new-nav");
|
|
186
|
-
if (entries.length <= 1) {
|
|
187
|
-
navEl.classList.add("hidden");
|
|
188
|
-
} else {
|
|
189
|
-
navEl.classList.remove("hidden");
|
|
190
|
-
dotsEl.innerHTML = "";
|
|
191
|
-
for (var i = 0; i < entries.length; i++) {
|
|
192
|
-
var dot = document.createElement("button");
|
|
193
|
-
dot.type = "button";
|
|
194
|
-
dot.className = "whats-new-dot" + (i === currentIndex ? " active" : "");
|
|
195
|
-
dot.setAttribute("aria-label", "Go to slide " + (i + 1));
|
|
196
|
-
(function (idx) {
|
|
197
|
-
dot.addEventListener("click", function (ev) {
|
|
198
|
-
ev.stopPropagation();
|
|
199
|
-
currentIndex = idx;
|
|
200
|
-
renderCarousel();
|
|
201
|
-
});
|
|
202
|
-
})(i);
|
|
203
|
-
dotsEl.appendChild(dot);
|
|
204
|
-
}
|
|
205
|
-
rootEl.querySelector(".whats-new-prev").disabled = (currentIndex === 0);
|
|
206
|
-
rootEl.querySelector(".whats-new-next").disabled = (currentIndex === entries.length - 1);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function go(delta) {
|
|
211
|
-
if (entries.length === 0) return;
|
|
212
|
-
currentIndex = Math.max(0, Math.min(entries.length - 1, currentIndex + delta));
|
|
213
|
-
renderCarousel();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function readMoreCurrent() {
|
|
217
|
-
var entry = entries[currentIndex];
|
|
218
|
-
if (!entry) return;
|
|
219
|
-
// Mark this one seen on the server and hand off to the article viewer
|
|
220
|
-
// via onReadMore. We don't mark the others seen - user might still
|
|
221
|
-
// want to see them as cards on a future connect.
|
|
222
|
-
markSeen(entry.id);
|
|
223
|
-
var id = entry.id;
|
|
224
|
-
closeCarousel();
|
|
225
|
-
if (typeof onReadMore === "function") {
|
|
226
|
-
try { onReadMore(id); } catch (e) {}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function closeAndMarkSeen() {
|
|
231
|
-
for (var i = 0; i < entries.length; i++) {
|
|
232
|
-
if (entries[i] && entries[i].id) markSeen(entries[i].id);
|
|
233
|
-
}
|
|
234
|
-
closeCarousel();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function closeCarousel() {
|
|
238
|
-
if (rootEl) {
|
|
239
|
-
rootEl.remove();
|
|
240
|
-
rootEl = null;
|
|
241
|
-
}
|
|
242
|
-
entries = [];
|
|
243
|
-
currentIndex = 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function markSeen(id) {
|
|
247
|
-
if (!id || dismissedThisSession[id]) return;
|
|
248
|
-
dismissedThisSession[id] = true;
|
|
249
|
-
var ws = getWs();
|
|
250
|
-
if (ws && ws.readyState === 1) {
|
|
251
|
-
try { ws.send(JSON.stringify({ type: "whats_new_seen", id: id })); } catch (e) {}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function onKeyDown(ev) {
|
|
256
|
-
if (!rootEl) return;
|
|
257
|
-
if (ev.key === "Escape") {
|
|
258
|
-
closeAndMarkSeen();
|
|
259
|
-
} else if (ev.key === "ArrowLeft") {
|
|
260
|
-
go(-1);
|
|
261
|
-
} else if (ev.key === "ArrowRight") {
|
|
262
|
-
go(1);
|
|
263
|
-
}
|
|
264
|
-
}
|
package/lib/whats-new-content.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// What's New entries.
|
|
2
|
-
//
|
|
3
|
-
// Pure content. Each entry becomes one card in the carousel popup and
|
|
4
|
-
// one row in the home page's "What's New" feed. Append a new entry to
|
|
5
|
-
// push it to users on their next connect; the viewer queues unseen
|
|
6
|
-
// entries and shows them as a carousel one user can flip through.
|
|
7
|
-
//
|
|
8
|
-
// Each entry:
|
|
9
|
-
// id - stable string. Used as the "seen" key. Never reuse an
|
|
10
|
-
// id for different content - users who already dismissed
|
|
11
|
-
// the old one will not see the new one.
|
|
12
|
-
// title - headline shown in the carousel card and home feed.
|
|
13
|
-
// publishedAt - YYYY-MM-DD. Used for sort order (newest first) in the
|
|
14
|
-
// home feed and the carousel.
|
|
15
|
-
// image - optional URL. If present, shown as a banner at the top
|
|
16
|
-
// of the carousel card and the home entry. Drop a file
|
|
17
|
-
// under lib/public/whats-new/ and reference it as
|
|
18
|
-
// "/whats-new/<filename>".
|
|
19
|
-
// summary - 1-2 sentences. The carousel card body. Keep it short.
|
|
20
|
-
// body - full HTML for the home-page detail view. Use the same
|
|
21
|
-
// small allowed vocabulary as tui-policy-modal-body:
|
|
22
|
-
// <p>, <ul>/<ol>/<li>, <strong>, <em>, <code>, <h3>, <a>.
|
|
23
|
-
|
|
24
|
-
var ENTRIES = [
|
|
25
|
-
{
|
|
26
|
-
id: "2026-06-tui-default",
|
|
27
|
-
title: "TUI becomes the default for Claude sessions",
|
|
28
|
-
publishedAt: "2026-05-25",
|
|
29
|
-
image: null,
|
|
30
|
-
popup: false, // superseded by "GUI is back"; keep in the home feed only
|
|
31
|
-
summary: "Anthropic is splitting Claude billing into Interactive and Programmatic buckets on June 15. To keep working with your existing plan, Clay opens Claude sessions in the embedded claude CLI by default from that date.",
|
|
32
|
-
body: '' +
|
|
33
|
-
'<p><strong>2026-06-15 Anthropic billing change.</strong> Claude subscriptions split into two buckets:</p>' +
|
|
34
|
-
'<ul>' +
|
|
35
|
-
'<li><strong>Interactive</strong> — <code>claude.ai</code> chat, the terminal <code>claude</code> CLI, Claude Cowork. Uses your existing plan limits.</li>' +
|
|
36
|
-
'<li><strong>Programmatic</strong> — Claude Agent SDK, <code>claude -p</code>, GitHub Actions, and third-party apps built on the SDK. Charged at full API rates against a small monthly credit (Pro $20 · Max 5x $100 · Max 20x $200), no rollover.</li>' +
|
|
37
|
-
'</ul>' +
|
|
38
|
-
'<p>Clay\'s original chat UI is SDK-driven, so every message lands in the Programmatic bucket. Heavy users would burn through the credit in days.</p>' +
|
|
39
|
-
'<p><strong>TUI mode</strong> runs the real <code>claude</code> CLI inside an embedded terminal. Usage stays in the Interactive bucket, so it counts against your existing plan instead of the SDK credit.</p>' +
|
|
40
|
-
'<h3>Want the SDK chat instead?</h3>' +
|
|
41
|
-
'<p>Switch back to GUI mode anytime:</p>' +
|
|
42
|
-
'<ol>' +
|
|
43
|
-
'<li>Open your avatar → <strong>Settings</strong> at the bottom of the sidebar.</li>' +
|
|
44
|
-
'<li>Find <strong>"Open Claude as terminal (TUI)"</strong> and toggle it <strong>off</strong>.</li>' +
|
|
45
|
-
'</ol>' +
|
|
46
|
-
'<p class="tui-policy-modal-links">' +
|
|
47
|
-
'<a href="https://the-decoder.com/claude-subscriptions-get-separate-budgets-for-programmatic-use-billed-at-full-api-prices/" target="_blank" rel="noopener noreferrer">Coverage at The Decoder</a>' +
|
|
48
|
-
' · ' +
|
|
49
|
-
'<a href="https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan" target="_blank" rel="noopener noreferrer">Anthropic help center</a>' +
|
|
50
|
-
' · ' +
|
|
51
|
-
'<a href="https://github.com/chadbyte/clay" target="_blank" rel="noopener noreferrer">github.com/chadbyte/clay</a>' +
|
|
52
|
-
'</p>',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "2026-06-mates-spinoff",
|
|
56
|
-
title: "Mates leaves the official feature set",
|
|
57
|
-
publishedAt: "2026-05-25",
|
|
58
|
-
image: null,
|
|
59
|
-
popup: false, // superseded by "GUI is back"; keep in the home feed only
|
|
60
|
-
summary: "June 15 will be Mates' last day as a first-class Clay feature. It continues as a separate standalone project so Clay can focus fully on being a developer tool.",
|
|
61
|
-
body: '' +
|
|
62
|
-
'<p>On <strong>2026-06-15</strong>, Mates leaves Clay\'s official feature set. Your existing Mates and their knowledge files will remain on disk; we will share migration instructions and a link to the new standalone project before the cutover.</p>' +
|
|
63
|
-
'<h3>Why</h3>' +
|
|
64
|
-
'<p>Mates grew into something broader than a coding companion - personas, debates, DMs, identity files. That is a different product shape than a focused dev tool, and trying to be both was slowing each side down.</p>' +
|
|
65
|
-
'<p>We are formally positioning Clay as a <strong>developer tool</strong>. From here on, every roadmap decision is judged by a single question: does it make Clay the sleekest, sharpest workspace on the AI-coding frontier? Mates no longer fits that bar cleanly, so it gets its own home where it can evolve on its own terms.</p>' +
|
|
66
|
-
'<p>Thanks for being here. Clay only gets sharper from here.</p>',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: "2026-07-gui-is-back",
|
|
70
|
-
title: "GUI is back as the default",
|
|
71
|
-
publishedAt: "2026-07-01",
|
|
72
|
-
image: null,
|
|
73
|
-
summary: "Anthropic reversed the June 15 billing split, so SDK usage is no longer billed separately. Clay opens Claude sessions in the GUI chat again by default. Project Mate DM shortcuts are hidden by default.",
|
|
74
|
-
body: '' +
|
|
75
|
-
'<p><strong>Good news: Anthropic walked back the June 15 billing change.</strong> The planned Interactive vs. Programmatic split (which would have charged Claude Agent SDK usage at full API rates) is off the table, so the reason we moved everyone to the TUI is gone.</p>' +
|
|
76
|
-
'<h3>GUI is the default again</h3>' +
|
|
77
|
-
'<p>New Claude sessions open in the <strong>GUI chat</strong> (SDK) by default, the way they did before. The embedded <code>claude</code> <strong>TUI</strong> is still there whenever you want it — toggle <strong>"Open Claude as terminal (TUI)"</strong> in Settings.</p>' +
|
|
78
|
-
'<h3>Project Mate DM shortcuts are optional</h3>' +
|
|
79
|
-
'<p>Mate DM shortcuts inside project workspaces are hidden by default and can be shown in <strong>Settings</strong>. Mates themselves remain available in Clay Home, together with their conversations, memory, debates, and project collaboration.</p>' +
|
|
80
|
-
'<p>Clay only gets sharper from here.</p>',
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
module.exports = { ENTRIES: ENTRIES };
|
package/lib/whats-new.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// What's New service: joins the static content registry with per-user
|
|
2
|
-
// "seen" state. The client viewer is content-agnostic; this is where the
|
|
3
|
-
// two halves meet on the server.
|
|
4
|
-
|
|
5
|
-
var content = require("./whats-new-content");
|
|
6
|
-
var users = require("./users");
|
|
7
|
-
|
|
8
|
-
function listEntries() {
|
|
9
|
-
return content.ENTRIES.slice();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function getUnseenForUser(userId) {
|
|
13
|
-
if (!userId) return [];
|
|
14
|
-
var seen = (typeof users.getWhatsNewSeenIds === "function") ? users.getWhatsNewSeenIds(userId) : [];
|
|
15
|
-
return content.ENTRIES.filter(function (e) {
|
|
16
|
-
return e && e.id && seen.indexOf(e.id) === -1;
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Combined state for the client: the full entries list (so the home
|
|
21
|
-
// feed can render previously-dismissed items) plus the subset of ids
|
|
22
|
-
// the user has not yet dismissed (so the carousel only auto-pops the
|
|
23
|
-
// unseen ones).
|
|
24
|
-
function getStateForUser(userId) {
|
|
25
|
-
var entries = content.ENTRIES.slice();
|
|
26
|
-
var seen = (typeof users.getWhatsNewSeenIds === "function" && userId) ? users.getWhatsNewSeenIds(userId) : [];
|
|
27
|
-
var unseenIds = [];
|
|
28
|
-
for (var i = 0; i < entries.length; i++) {
|
|
29
|
-
// popup:false entries stay in the home feed (entries) as history but never
|
|
30
|
-
// auto-pop in the carousel, so superseded announcements don't resurface.
|
|
31
|
-
if (entries[i] && entries[i].popup === false) continue;
|
|
32
|
-
if (entries[i] && entries[i].id && seen.indexOf(entries[i].id) === -1) {
|
|
33
|
-
unseenIds.push(entries[i].id);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return { entries: entries, unseenIds: unseenIds };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function markSeen(userId, entryId) {
|
|
40
|
-
if (!userId || !entryId) return { error: "missing" };
|
|
41
|
-
if (typeof users.markWhatsNewSeen !== "function") return { error: "unsupported" };
|
|
42
|
-
// Reject ids the server doesn't know about so a malformed client can't
|
|
43
|
-
// pollute the seen-list with arbitrary strings.
|
|
44
|
-
var known = false;
|
|
45
|
-
for (var i = 0; i < content.ENTRIES.length; i++) {
|
|
46
|
-
if (content.ENTRIES[i] && content.ENTRIES[i].id === entryId) { known = true; break; }
|
|
47
|
-
}
|
|
48
|
-
if (!known) return { error: "unknown_entry" };
|
|
49
|
-
return users.markWhatsNewSeen(userId, entryId);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
module.exports = {
|
|
53
|
-
listEntries: listEntries,
|
|
54
|
-
getUnseenForUser: getUnseenForUser,
|
|
55
|
-
getStateForUser: getStateForUser,
|
|
56
|
-
markSeen: markSeen,
|
|
57
|
-
};
|