@sparkelf/dsh-group-chat 0.0.0-stage → 0.1.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/README.md +90 -2
- package/cordis.patch.yml +3 -0
- package/lib/client.js +4238 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +357 -0
- package/lib/index.js +10024 -0
- package/package.json +98 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,4238 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@sparkelf/dsh-group-chat",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_dom = require("react-dom");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#region src/client/api.ts
|
|
12
|
+
/**
|
|
13
|
+
* Room-page transport.
|
|
14
|
+
*
|
|
15
|
+
* The browser talks only to the Host plugin's own route. The Host holds the
|
|
16
|
+
* workspace credential and forwards to the server under the workspace identity, so
|
|
17
|
+
* this module never sees a token and cannot disagree with the agent about who the
|
|
18
|
+
* caller is.
|
|
19
|
+
*
|
|
20
|
+
* @module @sparkelf/dsh-group-chat/client/api
|
|
21
|
+
*/
|
|
22
|
+
/** The Host route registered by this plugin's own Host half. */
|
|
23
|
+
const GC_API = "/dsh-group-chat/api";
|
|
24
|
+
/** A failure the page renders as copy rather than as an exception. */
|
|
25
|
+
var RoomError = class extends Error {
|
|
26
|
+
status;
|
|
27
|
+
constructor(message, status) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.name = "RoomError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const call = async (path, init) => {
|
|
34
|
+
let response;
|
|
35
|
+
try {
|
|
36
|
+
response = await fetch(`${GC_API}${path}`, {
|
|
37
|
+
...init,
|
|
38
|
+
headers: {
|
|
39
|
+
...init?.body === void 0 ? {} : { "content-type": "application/json" },
|
|
40
|
+
...init?.headers
|
|
41
|
+
},
|
|
42
|
+
credentials: "same-origin"
|
|
43
|
+
});
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
throw new RoomError("unreachable", 0);
|
|
46
|
+
}
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const status = response.status;
|
|
49
|
+
if (status === 503) throw new RoomError("credentialMissing", status);
|
|
50
|
+
if (status === 401 || status === 403) throw new RoomError("credentialRejected", status);
|
|
51
|
+
if (status === 404) throw new RoomError("notConfigured", status);
|
|
52
|
+
if (status === 409) throw new RoomError("ownerCannotLeave", status);
|
|
53
|
+
throw new RoomError("unreachable", status);
|
|
54
|
+
}
|
|
55
|
+
if (response.status === 204) return void 0;
|
|
56
|
+
return await response.json();
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The account this workspace acts as.
|
|
60
|
+
*
|
|
61
|
+
* The message renderer needs it to tell the operator's own messages from another member's,
|
|
62
|
+
* and the browser holds no credential of its own to ask DataOps directly — the Host resolves
|
|
63
|
+
* it from the same identity that decides message routing, so the two cannot disagree.
|
|
64
|
+
*
|
|
65
|
+
* @returns the account id, or `undefined` when the Host cannot identify this workspace.
|
|
66
|
+
*/
|
|
67
|
+
const readSelf = async () => {
|
|
68
|
+
try {
|
|
69
|
+
const me = await call("/me");
|
|
70
|
+
if (typeof me?.id !== "string" || me.id.length === 0) return void 0;
|
|
71
|
+
const administrator = Array.isArray(me.roleCodes) && me.roleCodes.some((code) => code === "管理员" || code === "超级管理员");
|
|
72
|
+
return {
|
|
73
|
+
id: me.id,
|
|
74
|
+
administrator
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
/** Rooms this workspace has joined. */
|
|
81
|
+
const listRooms = () => call("/rooms");
|
|
82
|
+
/**
|
|
83
|
+
* Rooms this account could join, as opposed to the ones it is already in.
|
|
84
|
+
*
|
|
85
|
+
* Kept separate from {@link listRooms} because the two answer different questions: the sidebar
|
|
86
|
+
* shows what you belong to, while this is consulted only when the operator is looking for
|
|
87
|
+
* something to join. Merging them would make every list read ask for rooms the account has no
|
|
88
|
+
* relationship with.
|
|
89
|
+
*
|
|
90
|
+
* @param query - optional narrowing keyword, applied by the server.
|
|
91
|
+
* @returns matching rooms, each carrying `joined` so the caller can tell them apart.
|
|
92
|
+
*/
|
|
93
|
+
const listDiscoverableRooms = (query) => {
|
|
94
|
+
const suffix = query === void 0 || query.trim().length === 0 ? "" : `?query=${encodeURIComponent(query.trim())}`;
|
|
95
|
+
return call(`/discover${suffix}`);
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Take a seat in a room the server allows this account to join.
|
|
99
|
+
*
|
|
100
|
+
* The decision to allow it is the server's: it re-reads discoverability, invite policy and
|
|
101
|
+
* membership at the moment of the press, so a room offered a minute ago may still be refused.
|
|
102
|
+
* That refusal arrives as a `RoomError` and is shown as-is rather than being pre-empted here.
|
|
103
|
+
*
|
|
104
|
+
* @param roomId - the room to join.
|
|
105
|
+
* @returns the server's answer, including whether the account was already a member.
|
|
106
|
+
*/
|
|
107
|
+
const joinRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/join`, { method: "POST" });
|
|
108
|
+
const listMembers = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/members`);
|
|
109
|
+
/**
|
|
110
|
+
* Accounts that match `query` and are not yet in the room.
|
|
111
|
+
*
|
|
112
|
+
* The search runs on the Host, which holds the credential: the browser has no DataOps token
|
|
113
|
+
* of its own, so a client-side filter over a full directory is not an option here. An empty
|
|
114
|
+
* query is valid and returns a browsable first page rather than nothing, which is what makes
|
|
115
|
+
* the picker usable before the operator has typed anything.
|
|
116
|
+
*/
|
|
117
|
+
const searchInviteCandidates = (roomId, query) => {
|
|
118
|
+
const suffix = query.length === 0 ? "" : `?q=${encodeURIComponent(query)}`;
|
|
119
|
+
return call(`/rooms/${encodeURIComponent(roomId)}/invite-candidates${suffix}`);
|
|
120
|
+
};
|
|
121
|
+
/** Preset names this deployment offers. An empty list means the picker falls back to typing. */
|
|
122
|
+
const readOptions = () => call("/options");
|
|
123
|
+
/** Child directories of `path`, for the working-directory picker. */
|
|
124
|
+
const listDirectories = (path) => {
|
|
125
|
+
const suffix = path === void 0 || path.length === 0 ? "" : `?path=${encodeURIComponent(path)}`;
|
|
126
|
+
return call(`/directories${suffix}`);
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Create a room.
|
|
130
|
+
*
|
|
131
|
+
* `cwd` and the preset are sent at creation because they decide the scope every member
|
|
132
|
+
* will share; the server does not accept them afterwards.
|
|
133
|
+
*/
|
|
134
|
+
const createRoom = (name, execution) => call("/rooms", {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
name,
|
|
138
|
+
...execution
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Set or clear a room's announcement.
|
|
143
|
+
*
|
|
144
|
+
* Only the room owner may write it; the server decides that, so a refusal is surfaced as-is
|
|
145
|
+
* rather than pre-empted here. An empty string clears the announcement — the server stores
|
|
146
|
+
* that as no announcement, which is why the panel treats `''` and `null` alike when reading.
|
|
147
|
+
*
|
|
148
|
+
* @param roomId - the room whose announcement to write.
|
|
149
|
+
* @param announcement - the new text, or an empty string to clear it.
|
|
150
|
+
*/
|
|
151
|
+
const setAnnouncement = (roomId, announcement) => call(`/rooms/${encodeURIComponent(roomId)}/announcement`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({ announcement })
|
|
154
|
+
});
|
|
155
|
+
const updateRoom = (roomId, patch) => call(`/rooms/${encodeURIComponent(roomId)}/update`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
body: JSON.stringify(patch)
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* Hand ownership of a room to another member.
|
|
161
|
+
*
|
|
162
|
+
* The reason this exists at all is that a room's owner cannot leave it: leaving would strand the
|
|
163
|
+
* room with nobody able to change its settings. Transferring is the way out, so the server owns
|
|
164
|
+
* the rule (owner only, target must be a member) and this only carries the request.
|
|
165
|
+
*/
|
|
166
|
+
const transferOwnership = (roomId, userId) => call(`/rooms/${encodeURIComponent(roomId)}/transfer`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
body: JSON.stringify({ userId })
|
|
169
|
+
});
|
|
170
|
+
const inviteMembers = (roomId, userIds) => call(`/rooms/${encodeURIComponent(roomId)}/members`, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: JSON.stringify({ userIds })
|
|
173
|
+
});
|
|
174
|
+
const leaveRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}/members/me`, { method: "DELETE" });
|
|
175
|
+
/** Reads the stored preferences. Never fails: an unreadable file reads as "nothing saved". */
|
|
176
|
+
const readPreferences = async () => {
|
|
177
|
+
try {
|
|
178
|
+
return await call("/preferences");
|
|
179
|
+
} catch {
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/** Stores the preferences and returns what the host settled on, clamped. */
|
|
184
|
+
const savePreferences = (patch) => call("/preferences", {
|
|
185
|
+
method: "PATCH",
|
|
186
|
+
body: JSON.stringify(patch)
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* List a room's shared folder.
|
|
190
|
+
*
|
|
191
|
+
* An empty `path` means this deployment has no shared tree, which the panel renders as
|
|
192
|
+
* "not available" rather than as an empty folder — the two read differently to a member.
|
|
193
|
+
*/
|
|
194
|
+
const listRoomFiles = (roomId, path) => call(`/rooms/${encodeURIComponent(roomId)}/files${path === void 0 || path.length === 0 ? "" : `?path=${encodeURIComponent(path)}`}`);
|
|
195
|
+
/** Read one file out of a room's shared folder. */
|
|
196
|
+
const getRoomFile = (roomId, path) => call(`/rooms/${encodeURIComponent(roomId)}/file-content?path=${encodeURIComponent(path)}`);
|
|
197
|
+
/**
|
|
198
|
+
* Upload one file into a room's shared folder.
|
|
199
|
+
*
|
|
200
|
+
* Base64 in JSON rather than multipart: the plugin's request path reads one JSON envelope for
|
|
201
|
+
* every operation, and a second body format would need its own parser, limits, and error
|
|
202
|
+
* mapping for no gain.
|
|
203
|
+
*
|
|
204
|
+
* @param roomId - the room whose folder receives the file.
|
|
205
|
+
* @param file - the browser's File object.
|
|
206
|
+
* @returns the written file.
|
|
207
|
+
*/
|
|
208
|
+
const uploadRoomFile = async (roomId, file) => {
|
|
209
|
+
const buffer = new Uint8Array(await file.arrayBuffer());
|
|
210
|
+
let binary = "";
|
|
211
|
+
for (let index = 0; index < buffer.length; index += 1) binary += String.fromCharCode(buffer[index]);
|
|
212
|
+
return call(`/rooms/${encodeURIComponent(roomId)}/files`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
name: file.name,
|
|
216
|
+
data: btoa(binary)
|
|
217
|
+
})
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Remove one member from a room.
|
|
222
|
+
*
|
|
223
|
+
* The reserved word `me` keeps its meaning: removal always names another account, and the
|
|
224
|
+
* server rejects the owner as a target. Leaving therefore never goes through this call.
|
|
225
|
+
*/
|
|
226
|
+
const removeMember = (roomId, userId) => call(`/rooms/${encodeURIComponent(roomId)}/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
|
|
227
|
+
/**
|
|
228
|
+
* Dissolve a room outright.
|
|
229
|
+
*
|
|
230
|
+
* Distinct from {@link leaveRoom}: leaving removes one membership and the room keeps existing
|
|
231
|
+
* for everyone else, while dissolving ends the room for every member. The server decides who
|
|
232
|
+
* may do it (the owner, or an administrator), so a refusal surfaces as copy rather than being
|
|
233
|
+
* pre-empted here.
|
|
234
|
+
*/
|
|
235
|
+
const dissolveRoom = (roomId) => call(`/rooms/${encodeURIComponent(roomId)}`, { method: "DELETE" });
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/client/rows.ts
|
|
238
|
+
/**
|
|
239
|
+
* The decisions the plugin's components make without a browser.
|
|
240
|
+
*
|
|
241
|
+
* They live outside their components on purpose. The components import the official
|
|
242
|
+
* primitives, which bring CSS modules that this repository's test runner cannot transform,
|
|
243
|
+
* so anything worth asserting has to be reachable without mounting a component. That
|
|
244
|
+
* constraint turns out to match the risk: what an operator sees — which rows survive a
|
|
245
|
+
* query, what a picture falls back to, who carries the owner tag, whether an empty
|
|
246
|
+
* announcement area means "write one" or "this deployment cannot store one" — is exactly
|
|
247
|
+
* what goes wrong quietly, and it is all decidable from plain values.
|
|
248
|
+
*
|
|
249
|
+
* @module @sparkelf/dsh-group-chat/client/rows
|
|
250
|
+
*/
|
|
251
|
+
/**
|
|
252
|
+
* The locale namespace the shipped presets publish their names in.
|
|
253
|
+
*
|
|
254
|
+
* Read as a foreign namespace rather than one this plugin registers: `standard` and the other
|
|
255
|
+
* three are declared by the platform, and their copy is owned there. Reprinting that copy here
|
|
256
|
+
* would freeze today's wording and drift from the hero chip the moment the platform changes it.
|
|
257
|
+
*/
|
|
258
|
+
const PRESET_LOCALE_NS = "settings.agentPreset";
|
|
259
|
+
/** Locale keys the shipped presets use, keyed by the id the registry publishes. */
|
|
260
|
+
const BUILT_IN_PRESET_KEYS = {
|
|
261
|
+
standard: {
|
|
262
|
+
name: "presetStandardName",
|
|
263
|
+
description: "presetStandardDescription"
|
|
264
|
+
},
|
|
265
|
+
ptc: {
|
|
266
|
+
name: "presetPtcName",
|
|
267
|
+
description: "presetPtcDescription"
|
|
268
|
+
},
|
|
269
|
+
minimal: {
|
|
270
|
+
name: "presetMinimalName",
|
|
271
|
+
description: "presetMinimalDescription"
|
|
272
|
+
},
|
|
273
|
+
cordis: {
|
|
274
|
+
name: "presetCordisName",
|
|
275
|
+
description: "presetCordisDescription"
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
/**
|
|
279
|
+
* A preset's display copy, following the platform's own rule.
|
|
280
|
+
*
|
|
281
|
+
* A shipped preset publishes no `name`, so its copy is a locale lookup; a preset that names
|
|
282
|
+
* itself in YAML owns its own words and is never translated. The split matters because the two
|
|
283
|
+
* arrive as the same `{ id, name? }` shape — treating a published name as a locale key would
|
|
284
|
+
* render the key itself, and treating a shipped id as self-named would render `standard`.
|
|
285
|
+
*
|
|
286
|
+
* @param preset - one roster row.
|
|
287
|
+
* @param t - lookup for the preset locale namespace; a missing key resolves to the key, so the
|
|
288
|
+
* fallback below is what keeps a renamed platform key from reaching the operator.
|
|
289
|
+
* @returns the name and optional description to draw.
|
|
290
|
+
*/
|
|
291
|
+
const presetDisplay = (preset, t) => {
|
|
292
|
+
const keys = preset.name === void 0 ? BUILT_IN_PRESET_KEYS[preset.id] : void 0;
|
|
293
|
+
if (keys === void 0) return {
|
|
294
|
+
name: preset.name ?? preset.id,
|
|
295
|
+
...preset.description === void 0 ? {} : { description: preset.description }
|
|
296
|
+
};
|
|
297
|
+
const name = t(keys.name);
|
|
298
|
+
const description = t(keys.description);
|
|
299
|
+
return {
|
|
300
|
+
name: name === keys.name ? preset.id : name,
|
|
301
|
+
...description === keys.description ? {} : { description }
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Filter rows by what the operator typed.
|
|
306
|
+
*
|
|
307
|
+
* Matching happens here rather than only at the source because the source's answer arrives a
|
|
308
|
+
* debounce behind the keystroke; re-filtering the rows already held narrows the list on the
|
|
309
|
+
* same frame instead of one request later. `secondary` participates because a member's handle
|
|
310
|
+
* is often what the operator knows them by.
|
|
311
|
+
*
|
|
312
|
+
* @param options - the rows currently held.
|
|
313
|
+
* @param query - the raw input.
|
|
314
|
+
* @returns the rows to draw, in their original order.
|
|
315
|
+
*/
|
|
316
|
+
const filterOptions = (options, query) => {
|
|
317
|
+
const needle = query.trim().toLowerCase();
|
|
318
|
+
if (needle.length === 0) return options;
|
|
319
|
+
return options.filter((option) => option.label.toLowerCase().includes(needle) || (option.secondary ?? "").toLowerCase().includes(needle));
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Add or remove one person from the invite selection.
|
|
323
|
+
*
|
|
324
|
+
* The selection is kept apart from the search result rows because the two answer different
|
|
325
|
+
* questions: the rows are "what the current query found", while the selection is "who will be
|
|
326
|
+
* invited". A narrowed query legitimately hides a person already picked, and deriving the
|
|
327
|
+
* selection from the visible rows would drop them the moment the operator typed another name.
|
|
328
|
+
*
|
|
329
|
+
* The row is stored whole, not by id, so a chip can still draw the name and picture after the
|
|
330
|
+
* search that produced it has been replaced.
|
|
331
|
+
*
|
|
332
|
+
* @param chosen - the current selection, in pick order.
|
|
333
|
+
* @param candidate - the row the operator pressed.
|
|
334
|
+
* @returns the new selection; a second press on the same id removes it, and identity is stable
|
|
335
|
+
* when the press changed nothing.
|
|
336
|
+
*/
|
|
337
|
+
const toggleChosen = (chosen, candidate) => chosen.some((member) => member.userId === candidate.userId) ? chosen.filter((member) => member.userId !== candidate.userId) : [...chosen, candidate];
|
|
338
|
+
/**
|
|
339
|
+
* Narrow the invite candidates to what the operator typed.
|
|
340
|
+
*
|
|
341
|
+
* The same rule the server applies, applied again to the rows already held, for one reason:
|
|
342
|
+
* the server's answer arrives a debounce behind the keystroke, and the list would otherwise
|
|
343
|
+
* sit still while the operator types. Re-filtering locally makes the narrowing immediate; the
|
|
344
|
+
* server request still happens, and its answer replaces these rows a moment later.
|
|
345
|
+
*
|
|
346
|
+
* Both the display name and the handle participate, because the handle is often what a
|
|
347
|
+
* colleague is known by — the server's search matches the same pair.
|
|
348
|
+
*
|
|
349
|
+
* @param candidates - the rows currently held, from the last answer.
|
|
350
|
+
* @param query - the raw input.
|
|
351
|
+
* @returns the rows to draw, in their original order.
|
|
352
|
+
*/
|
|
353
|
+
const filterCandidates = (candidates, query) => {
|
|
354
|
+
const needle = query.trim().toLowerCase();
|
|
355
|
+
if (needle.length === 0) return candidates;
|
|
356
|
+
return candidates.filter((candidate) => candidate.displayName.toLowerCase().includes(needle) || candidate.username.toLowerCase().includes(needle));
|
|
357
|
+
};
|
|
358
|
+
/**
|
|
359
|
+
* Whether one candidate row is part of the selection.
|
|
360
|
+
*
|
|
361
|
+
* @param chosen - the current selection.
|
|
362
|
+
* @param userId - the row's account id.
|
|
363
|
+
* @returns whether the row draws as selected.
|
|
364
|
+
*/
|
|
365
|
+
const isChosen = (chosen, userId) => chosen.some((member) => member.userId === userId);
|
|
366
|
+
/**
|
|
367
|
+
* Up to two initials from a display name.
|
|
368
|
+
*
|
|
369
|
+
* Names arriving here are DataOps display names, which are as often Chinese as Latin. A CJK
|
|
370
|
+
* name has no spaces to split on, so the first two characters are taken as-is; a Latin name
|
|
371
|
+
* takes the first letter of its first two words. Both paths return at most two glyphs, which
|
|
372
|
+
* is what keeps the circle from overflowing at either size.
|
|
373
|
+
*
|
|
374
|
+
* @param name - the display name, possibly empty.
|
|
375
|
+
* @returns one or two characters, or `'?'` when the name carries nothing to draw.
|
|
376
|
+
*/
|
|
377
|
+
const initialsOf = (name) => {
|
|
378
|
+
const trimmed = name.trim();
|
|
379
|
+
if (trimmed.length === 0) return "?";
|
|
380
|
+
if (/^[\u3400-\u9fff\uf900-\ufaff]/u.test(trimmed)) return [...trimmed].slice(0, 2).join("");
|
|
381
|
+
const words = trimmed.split(/\s+/u).filter((word) => word.length > 0);
|
|
382
|
+
if (words.length === 1) return [...words[0]].slice(0, 2).join("").toUpperCase();
|
|
383
|
+
return (words[0][0] + words[1][0]).toUpperCase();
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* The rooms worth offering as joinable.
|
|
387
|
+
*
|
|
388
|
+
* Two filters, for two different failures. `joined` is belt-and-braces: the Host already strips
|
|
389
|
+
* joined rooms from discovery, but this list arrives from a request the sidebar did not wait
|
|
390
|
+
* for, so a room the operator joined a moment ago can still be in the response.
|
|
391
|
+
*
|
|
392
|
+
* The member-id check covers the other window: the discovery read and the sidebar's own list
|
|
393
|
+
* come from separate requests, so a room can be in both for a moment. Without it the same room
|
|
394
|
+
* would appear twice, and the copy under the discovery heading would offer a Join button that
|
|
395
|
+
* can only fail.
|
|
396
|
+
*
|
|
397
|
+
* @param discoverable - what the discovery read returned.
|
|
398
|
+
* @param memberIds - ids of the rooms the sidebar is already showing.
|
|
399
|
+
* @returns the rooms to render under the discovery heading.
|
|
400
|
+
*/
|
|
401
|
+
const joinableRooms = (discoverable, memberIds) => discoverable.filter((room) => !room.joined && !memberIds.has(room.id));
|
|
402
|
+
/**
|
|
403
|
+
* Whether a member owns the room.
|
|
404
|
+
*
|
|
405
|
+
* `owner` is the field every server has ever sent, `role` is the one the announcement work
|
|
406
|
+
* added. Preferring `role` and falling back to `owner` keeps the tag correct against either
|
|
407
|
+
* server, and costs nothing: the two carry the same fact.
|
|
408
|
+
*
|
|
409
|
+
* @param member - one roster row.
|
|
410
|
+
* @returns whether the row is the room's owner.
|
|
411
|
+
*/
|
|
412
|
+
const memberIsOwner = (member) => member.role === void 0 ? member.owner === true : member.role === "owner";
|
|
413
|
+
/**
|
|
414
|
+
* Roster rows for the panel, owner pinned first.
|
|
415
|
+
*
|
|
416
|
+
* Only the owner is moved. Everyone else keeps the order the server sent, which is join
|
|
417
|
+
* order — a roster re-sorted alphabetically would scramble the one fact a reader can
|
|
418
|
+
* actually see in it (who has been here longest), and would do so differently for a CJK
|
|
419
|
+
* name than for a Latin one.
|
|
420
|
+
*
|
|
421
|
+
* @param members - the roster as the store holds it.
|
|
422
|
+
* @param selfId - this workspace's account id, `undefined` while identity is still unknown.
|
|
423
|
+
* @returns rows to draw.
|
|
424
|
+
*/
|
|
425
|
+
const memberRows = (members, selfId) => {
|
|
426
|
+
return members.map((member) => ({
|
|
427
|
+
userId: member.userId,
|
|
428
|
+
name: member.displayName.length > 0 ? member.displayName : member.userId,
|
|
429
|
+
avatarUrl: member.avatarUrl,
|
|
430
|
+
owner: memberIsOwner(member),
|
|
431
|
+
self: selfId !== void 0 && member.userId === selfId
|
|
432
|
+
})).slice().sort((left, right) => Number(right.owner) - Number(left.owner));
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Narrow the roster to what the operator typed.
|
|
436
|
+
*
|
|
437
|
+
* The account id participates because it is what another member is often known by — the
|
|
438
|
+
* invite picker searches the handle for the same reason.
|
|
439
|
+
*
|
|
440
|
+
* @param rows - the roster rows currently held.
|
|
441
|
+
* @param query - the raw input.
|
|
442
|
+
* @returns rows to draw, in their order.
|
|
443
|
+
*/
|
|
444
|
+
const filterMemberRows = (rows, query) => {
|
|
445
|
+
const needle = query.trim().toLowerCase();
|
|
446
|
+
if (needle.length === 0) return rows;
|
|
447
|
+
return rows.filter((row) => row.name.toLowerCase().includes(needle) || row.userId.toLowerCase().includes(needle));
|
|
448
|
+
};
|
|
449
|
+
/**
|
|
450
|
+
* Read one room's announcement into the state its area draws.
|
|
451
|
+
*
|
|
452
|
+
* @param announcement - the room summary's field.
|
|
453
|
+
* @returns the state to render.
|
|
454
|
+
*/
|
|
455
|
+
const announcementView = (announcement) => {
|
|
456
|
+
if (announcement === void 0) return { state: "unavailable" };
|
|
457
|
+
const text = (announcement ?? "").trim();
|
|
458
|
+
return text.length === 0 ? { state: "empty" } : {
|
|
459
|
+
state: "text",
|
|
460
|
+
text
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
/**
|
|
464
|
+
* Whether a failed write means this deployment has no announcement feature.
|
|
465
|
+
*
|
|
466
|
+
* A rolled-back deployment answers the route with 404 (no route) or 501/405 (a route that
|
|
467
|
+
* refuses the method). Those are the only failures that should switch the area to
|
|
468
|
+
* "unavailable": a 401 or a 5xx is this request's problem, and reporting it as a missing
|
|
469
|
+
* feature would send the operator looking at the wrong thing.
|
|
470
|
+
*
|
|
471
|
+
* @param error - whatever the write threw.
|
|
472
|
+
* @returns whether the feature should be regarded as absent.
|
|
473
|
+
*/
|
|
474
|
+
const announcementMissing = (error) => error instanceof RoomError && (error.status === 404 || error.status === 405 || error.status === 501);
|
|
475
|
+
//#endregion
|
|
476
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/Avatar.module.css.mjs
|
|
477
|
+
const css$8 = ".Ohch6W_avatar{letter-spacing:.02em;color:#fff;background:var(--dsw-static-blue-600,#2563eb);user-select:none;border-radius:50%;flex:none;justify-content:center;align-items:center;font-weight:600;display:inline-flex;overflow:hidden}.Ohch6W_md{width:30px;height:30px;font-size:12px;line-height:30px}.Ohch6W_sm{width:24px;height:24px;font-size:10px;line-height:24px}.Ohch6W_stack{flex:none;display:inline-flex;position:relative}.Ohch6W_over{background:0 0;position:absolute;inset:0}img.Ohch6W_avatar{object-fit:cover;background:0 0}";
|
|
478
|
+
const tagId$8 = "@sparkelf/dsh-group-chat/Avatar.module.css";
|
|
479
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
|
|
480
|
+
const tag = document.createElement("style");
|
|
481
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
482
|
+
tag.dataset.pluginCss = tagId$8;
|
|
483
|
+
tag.textContent = css$8;
|
|
484
|
+
document.head.appendChild(tag);
|
|
485
|
+
}
|
|
486
|
+
var Avatar_module_css_default = {
|
|
487
|
+
"sm": "Ohch6W_sm",
|
|
488
|
+
"over": "Ohch6W_over",
|
|
489
|
+
"md": "Ohch6W_md",
|
|
490
|
+
"avatar": "Ohch6W_avatar",
|
|
491
|
+
"stack": "Ohch6W_stack"
|
|
492
|
+
};
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/client/Avatar.tsx
|
|
495
|
+
/** One member's picture, or their initials when there is no picture to show. */
|
|
496
|
+
const Avatar = (props) => {
|
|
497
|
+
const { name, src, size = "md" } = props;
|
|
498
|
+
const className = `${Avatar_module_css_default["avatar"]} ${size === "sm" ? Avatar_module_css_default["sm"] : Avatar_module_css_default["md"]}`;
|
|
499
|
+
const fallback = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
500
|
+
className,
|
|
501
|
+
role: "img",
|
|
502
|
+
"aria-label": name,
|
|
503
|
+
children: initialsOf(name)
|
|
504
|
+
});
|
|
505
|
+
if (typeof src !== "string" || src.length === 0) return fallback;
|
|
506
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
507
|
+
className: Avatar_module_css_default["stack"],
|
|
508
|
+
children: [fallback, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
509
|
+
className: `${Avatar_module_css_default["avatar"]} ${size === "sm" ? Avatar_module_css_default["sm"] : Avatar_module_css_default["md"]} ${Avatar_module_css_default["over"]}`,
|
|
510
|
+
src,
|
|
511
|
+
alt: "",
|
|
512
|
+
"aria-hidden": "true",
|
|
513
|
+
onError: (event) => {
|
|
514
|
+
event.currentTarget.style.display = "none";
|
|
515
|
+
}
|
|
516
|
+
})]
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/Picker.module.css.mjs
|
|
521
|
+
const css$7 = ".sKjJ-a_box{flex-direction:column;min-width:0;display:flex;position:relative}.sKjJ-a_list{z-index:10;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-base,var(--dsw-alias-interactive-bg-hover));max-height:240px;padding:4px;position:absolute;top:calc(100% + 4px);left:0;right:0;overflow-y:auto;box-shadow:0 8px 24px #0000003d}.sKjJ-a_row,.sKjJ-a_rowActive{border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:6px 8px;font-size:13px;line-height:18px;display:flex}.sKjJ-a_rowActive{background:var(--dsw-alias-interactive-bg-hover)}.sKjJ-a_label{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.sKjJ-a_secondary{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px}.sKjJ-a_note{color:var(--dsw-alias-label-tertiary);margin:6px 8px;font-size:12px;line-height:17px}";
|
|
522
|
+
const tagId$7 = "@sparkelf/dsh-group-chat/Picker.module.css";
|
|
523
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$7) + "]") === null) {
|
|
524
|
+
const tag = document.createElement("style");
|
|
525
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
526
|
+
tag.dataset.pluginCss = tagId$7;
|
|
527
|
+
tag.textContent = css$7;
|
|
528
|
+
document.head.appendChild(tag);
|
|
529
|
+
}
|
|
530
|
+
var Picker_module_css_default = {
|
|
531
|
+
"note": "sKjJ-a_note",
|
|
532
|
+
"secondary": "sKjJ-a_secondary",
|
|
533
|
+
"list": "sKjJ-a_list",
|
|
534
|
+
"row": "sKjJ-a_row",
|
|
535
|
+
"box": "sKjJ-a_box",
|
|
536
|
+
"rowActive": "sKjJ-a_rowActive",
|
|
537
|
+
"label": "sKjJ-a_label"
|
|
538
|
+
};
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/client/Picker.tsx
|
|
541
|
+
/**
|
|
542
|
+
* A searchable list of choices, for the fields that used to be free text.
|
|
543
|
+
*
|
|
544
|
+
* Three of the room dialogs ask for values the deployment already knows — who can be
|
|
545
|
+
* invited, which preset to run, which directory to work in — and each used to be an empty
|
|
546
|
+
* box that only accepted the answer if it was spelled exactly right. A wrong userId failed
|
|
547
|
+
* only after the invite was submitted, and a wrong directory silently became a room the
|
|
548
|
+
* agent could not work in. This component is the shared answer so all three behave alike
|
|
549
|
+
* rather than each growing its own behaviour.
|
|
550
|
+
*
|
|
551
|
+
* It is deliberately not a `<datalist>`: the options come from an async source, the row
|
|
552
|
+
* needs a picture and a secondary line, and a datalist cannot group or report why it is
|
|
553
|
+
* empty. It is also not built on the official primitives because none of them is a
|
|
554
|
+
* combobox — the package ships Button, Input, Menu and friends, but no searchable select.
|
|
555
|
+
*
|
|
556
|
+
* @module @sparkelf/dsh-group-chat/client/Picker
|
|
557
|
+
*/
|
|
558
|
+
/**
|
|
559
|
+
* Sentinel id for the "use what I typed" row.
|
|
560
|
+
*
|
|
561
|
+
* A real id cannot collide with it because account ids, preset names and paths are all
|
|
562
|
+
* printable, so a NUL is outside every id space this component is handed.
|
|
563
|
+
*/
|
|
564
|
+
const TYPED_ROW_ID = "\0typed";
|
|
565
|
+
/**
|
|
566
|
+
* A text field with a filtered list beneath it.
|
|
567
|
+
*
|
|
568
|
+
* Keyboard support is the reason this is not a plain list of buttons: ↑/↓ move the highlight
|
|
569
|
+
* without leaving the field, Enter takes the highlighted row, and Escape first closes the
|
|
570
|
+
* list and only then reaches the dialog, so a stray Escape while searching does not discard
|
|
571
|
+
* the whole dialog.
|
|
572
|
+
*/
|
|
573
|
+
const Picker = (props) => {
|
|
574
|
+
const { options, placeholder, emptyHint, loadingHint, loading = false, onQueryChange, onTypedCommit, typedCommitLabel, onPick, value, autoFocus = false } = props;
|
|
575
|
+
const [query, setQuery] = (0, react.useState)(value ?? "");
|
|
576
|
+
const [open, setOpen] = (0, react.useState)(autoFocus);
|
|
577
|
+
const [active, setActive] = (0, react.useState)(0);
|
|
578
|
+
const boxRef = (0, react.useRef)(null);
|
|
579
|
+
(0, react.useEffect)(() => {
|
|
580
|
+
setQuery(value ?? "");
|
|
581
|
+
}, [value]);
|
|
582
|
+
const visible = [...onTypedCommit !== void 0 && query.trim().length > 0 ? [{
|
|
583
|
+
id: TYPED_ROW_ID,
|
|
584
|
+
label: typedCommitLabel?.(query.trim()) ?? query.trim()
|
|
585
|
+
}] : [], ...filterOptions(options, query)];
|
|
586
|
+
(0, react.useEffect)(() => {
|
|
587
|
+
setActive(0);
|
|
588
|
+
}, [query]);
|
|
589
|
+
const pick = (0, react.useCallback)((option) => {
|
|
590
|
+
if (option === void 0) return;
|
|
591
|
+
if (option.id === TYPED_ROW_ID) {
|
|
592
|
+
onTypedCommit?.(query.trim());
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
onPick(option);
|
|
596
|
+
}, [
|
|
597
|
+
onPick,
|
|
598
|
+
onTypedCommit,
|
|
599
|
+
query
|
|
600
|
+
]);
|
|
601
|
+
(0, react.useEffect)(() => {
|
|
602
|
+
if (!open) return;
|
|
603
|
+
const onPointerDown = (event) => {
|
|
604
|
+
if (boxRef.current !== null && !boxRef.current.contains(event.target)) setOpen(false);
|
|
605
|
+
};
|
|
606
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
607
|
+
return () => {
|
|
608
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
609
|
+
};
|
|
610
|
+
}, [open]);
|
|
611
|
+
const onKeyDown = (event) => {
|
|
612
|
+
if (event.key === "ArrowDown") {
|
|
613
|
+
event.preventDefault();
|
|
614
|
+
setOpen(true);
|
|
615
|
+
setActive((current) => visible.length === 0 ? 0 : (current + 1) % visible.length);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (event.key === "ArrowUp") {
|
|
619
|
+
event.preventDefault();
|
|
620
|
+
setActive((current) => visible.length === 0 ? 0 : (current - 1 + visible.length) % visible.length);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (event.key === "Enter") {
|
|
624
|
+
if (open && visible.length > 0) {
|
|
625
|
+
event.preventDefault();
|
|
626
|
+
pick(visible[active]);
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (event.key === "Escape" && open) {
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
event.stopPropagation();
|
|
633
|
+
setOpen(false);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
637
|
+
className: Picker_module_css_default["box"],
|
|
638
|
+
ref: boxRef,
|
|
639
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
640
|
+
value: query,
|
|
641
|
+
autoFocus,
|
|
642
|
+
placeholder,
|
|
643
|
+
role: "combobox",
|
|
644
|
+
"aria-expanded": open,
|
|
645
|
+
"aria-autocomplete": "list",
|
|
646
|
+
onFocus: () => {
|
|
647
|
+
setOpen(true);
|
|
648
|
+
},
|
|
649
|
+
onChange: (event) => {
|
|
650
|
+
setQuery(event.target.value);
|
|
651
|
+
setOpen(true);
|
|
652
|
+
onQueryChange?.(event.target.value);
|
|
653
|
+
},
|
|
654
|
+
onKeyDown
|
|
655
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
656
|
+
className: Picker_module_css_default["list"],
|
|
657
|
+
role: "listbox",
|
|
658
|
+
children: [
|
|
659
|
+
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
660
|
+
className: Picker_module_css_default["note"],
|
|
661
|
+
children: loadingHint ?? ""
|
|
662
|
+
}),
|
|
663
|
+
!loading && visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
664
|
+
className: Picker_module_css_default["note"],
|
|
665
|
+
children: emptyHint ?? ""
|
|
666
|
+
}),
|
|
667
|
+
!loading && visible.map((option, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
668
|
+
type: "button",
|
|
669
|
+
role: "option",
|
|
670
|
+
"aria-selected": index === active,
|
|
671
|
+
className: index === active ? Picker_module_css_default["rowActive"] : Picker_module_css_default["row"],
|
|
672
|
+
onMouseEnter: () => {
|
|
673
|
+
setActive(index);
|
|
674
|
+
},
|
|
675
|
+
onClick: () => {
|
|
676
|
+
pick(option);
|
|
677
|
+
},
|
|
678
|
+
children: [
|
|
679
|
+
option.avatarId !== void 0 || option.avatarUrl !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
680
|
+
name: option.label,
|
|
681
|
+
src: option.avatarUrl,
|
|
682
|
+
size: "sm"
|
|
683
|
+
}) : null,
|
|
684
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
685
|
+
className: Picker_module_css_default["label"],
|
|
686
|
+
children: option.label
|
|
687
|
+
}),
|
|
688
|
+
option.secondary !== void 0 && option.secondary.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
689
|
+
className: Picker_module_css_default["secondary"],
|
|
690
|
+
children: option.secondary
|
|
691
|
+
})
|
|
692
|
+
]
|
|
693
|
+
}, option.id))
|
|
694
|
+
]
|
|
695
|
+
})]
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/PresetSelect.module.css.mjs
|
|
700
|
+
const css$6 = ".xjnmxG_trigger{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer;background:0 0;align-items:center;gap:6px;padding:6px 10px;font-size:13px;line-height:18px;display:flex}.xjnmxG_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.xjnmxG_triggerLabel{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.xjnmxG_trigger:after{border-right:1.5px solid var(--dsw-alias-label-tertiary);border-bottom:1.5px solid var(--dsw-alias-label-tertiary);content:\"\";flex:none;width:6px;height:6px;margin-left:2px;transform:translateY(-1px)rotate(45deg)}.xjnmxG_row{flex-direction:column;gap:2px;min-width:0;display:flex}.xjnmxG_name{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.xjnmxG_description{color:var(--dsw-alias-label-tertiary);white-space:normal;font-size:12px;line-height:17px}";
|
|
701
|
+
const tagId$6 = "@sparkelf/dsh-group-chat/PresetSelect.module.css";
|
|
702
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$6) + "]") === null) {
|
|
703
|
+
const tag = document.createElement("style");
|
|
704
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
705
|
+
tag.dataset.pluginCss = tagId$6;
|
|
706
|
+
tag.textContent = css$6;
|
|
707
|
+
document.head.appendChild(tag);
|
|
708
|
+
}
|
|
709
|
+
var PresetSelect_module_css_default = {
|
|
710
|
+
"description": "xjnmxG_description",
|
|
711
|
+
"triggerLabel": "xjnmxG_triggerLabel",
|
|
712
|
+
"row": "xjnmxG_row",
|
|
713
|
+
"name": "xjnmxG_name",
|
|
714
|
+
"trigger": "xjnmxG_trigger"
|
|
715
|
+
};
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/client/PresetSelect.tsx
|
|
718
|
+
/**
|
|
719
|
+
* The agent-preset field, built on the platform's own menu.
|
|
720
|
+
*
|
|
721
|
+
* This is the same control the new-session screen shows beside the workspace picker: the
|
|
722
|
+
* platform paints that one with `Menu` and draws each row as a name over its description.
|
|
723
|
+
* Reproducing the choice as a text box — or as a differently-shaped list — would make the same
|
|
724
|
+
* concept look like two things depending on where the operator met it, so the dialog borrows
|
|
725
|
+
* the platform's materials instead:
|
|
726
|
+
*
|
|
727
|
+
* - `Menu` for the dropdown, so the keyboard walk, outside-click dismissal and the portaled
|
|
728
|
+
* list behave exactly as they do on the hero chip.
|
|
729
|
+
* - The platform's `settings.agentPreset` locale namespace for the shipped presets' names and
|
|
730
|
+
* descriptions. That copy is owned by the platform; reprinting it here would freeze today's
|
|
731
|
+
* wording and drift the moment the platform rewords it.
|
|
732
|
+
*
|
|
733
|
+
* What this deliberately does not do is import the platform's preset component: its props are
|
|
734
|
+
* bound to the `conversation.hero.agentPreset` slot and carry a session plus a staged-selection
|
|
735
|
+
* store this dialog has neither of. The roster comes from the Host instead, which reads the
|
|
736
|
+
* same registry that slot renders.
|
|
737
|
+
*
|
|
738
|
+
* @module @sparkelf/dsh-group-chat/client/PresetSelect
|
|
739
|
+
*/
|
|
740
|
+
/**
|
|
741
|
+
* Render the preset trigger and its menu.
|
|
742
|
+
* @param props - roster, current choice, and the platform copy lookup.
|
|
743
|
+
* @returns the anchored dropdown.
|
|
744
|
+
*/
|
|
745
|
+
const PresetSelect = (props) => {
|
|
746
|
+
const { presets, value, t, defaultLabel, onChange } = props;
|
|
747
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
748
|
+
const chosen = presets.find((preset) => preset.id === value);
|
|
749
|
+
const trigger = chosen === void 0 ? defaultLabel : presetDisplay(chosen, t).name;
|
|
750
|
+
const entryOf = (preset) => {
|
|
751
|
+
const { name, description } = presetDisplay(preset, t);
|
|
752
|
+
return {
|
|
753
|
+
id: preset.id,
|
|
754
|
+
disabled: preset.broken !== void 0,
|
|
755
|
+
label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
756
|
+
className: PresetSelect_module_css_default["row"],
|
|
757
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
758
|
+
className: PresetSelect_module_css_default["name"],
|
|
759
|
+
children: name
|
|
760
|
+
}), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
761
|
+
className: PresetSelect_module_css_default["description"],
|
|
762
|
+
children: description
|
|
763
|
+
})]
|
|
764
|
+
})
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
const items = [...presets.filter((preset) => preset.name === void 0).map(entryOf), ...presets.filter((preset) => preset.name !== void 0).map(entryOf)];
|
|
768
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
769
|
+
open,
|
|
770
|
+
portal: true,
|
|
771
|
+
side: "bottom",
|
|
772
|
+
align: "start",
|
|
773
|
+
items,
|
|
774
|
+
selectedId: value.length === 0 ? void 0 : value,
|
|
775
|
+
onSelect: (id) => {
|
|
776
|
+
onChange(id);
|
|
777
|
+
setOpen(false);
|
|
778
|
+
},
|
|
779
|
+
onClose: () => {
|
|
780
|
+
setOpen(false);
|
|
781
|
+
},
|
|
782
|
+
footer: [{
|
|
783
|
+
id: "",
|
|
784
|
+
label: defaultLabel
|
|
785
|
+
}],
|
|
786
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
787
|
+
type: "button",
|
|
788
|
+
className: PresetSelect_module_css_default["trigger"],
|
|
789
|
+
"aria-haspopup": "menu",
|
|
790
|
+
"aria-expanded": open,
|
|
791
|
+
onClick: () => {
|
|
792
|
+
setOpen((current) => !current);
|
|
793
|
+
},
|
|
794
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
795
|
+
className: PresetSelect_module_css_default["triggerLabel"],
|
|
796
|
+
children: trigger
|
|
797
|
+
})
|
|
798
|
+
})
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
//#endregion
|
|
802
|
+
//#region src/client/store.ts
|
|
803
|
+
/**
|
|
804
|
+
* The room list both client surfaces read.
|
|
805
|
+
*
|
|
806
|
+
* It lives here rather than inside either component because the sidebar region and the
|
|
807
|
+
* room page must never show different lists. It is a plain observable, not a framework
|
|
808
|
+
* store: both consumers are components this plugin already owns.
|
|
809
|
+
*
|
|
810
|
+
* The slot caches the value its `inject` factory returns, so a snapshot taken at inject
|
|
811
|
+
* time would freeze at the first read. Consumers therefore subscribe through
|
|
812
|
+
* `useSyncExternalStore` and re-read on every change.
|
|
813
|
+
*
|
|
814
|
+
* @module @sparkelf/dsh-group-chat/client/store
|
|
815
|
+
*/
|
|
816
|
+
/**
|
|
817
|
+
* Create the shared store.
|
|
818
|
+
* @returns a store whose `getState` returns a stable reference between changes, which is
|
|
819
|
+
* what `useSyncExternalStore` requires to avoid a render loop.
|
|
820
|
+
*/
|
|
821
|
+
function createRoomStore() {
|
|
822
|
+
let state = {
|
|
823
|
+
rooms: void 0,
|
|
824
|
+
activeRoomId: void 0,
|
|
825
|
+
failure: void 0,
|
|
826
|
+
searchQuery: "",
|
|
827
|
+
selfId: void 0,
|
|
828
|
+
administrator: false,
|
|
829
|
+
members: {},
|
|
830
|
+
pendingCreate: false,
|
|
831
|
+
discoverable: void 0,
|
|
832
|
+
dialog: void 0
|
|
833
|
+
};
|
|
834
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
835
|
+
const set = (patch) => {
|
|
836
|
+
state = {
|
|
837
|
+
...state,
|
|
838
|
+
...patch
|
|
839
|
+
};
|
|
840
|
+
for (const listener of listeners) listener();
|
|
841
|
+
};
|
|
842
|
+
/**
|
|
843
|
+
* Read one room's roster into the store.
|
|
844
|
+
*
|
|
845
|
+
* A failure leaves the previous roster in place rather than clearing it: the room is still
|
|
846
|
+
* readable without names (the renderer falls back to an unlabelled bubble), and a stale name is
|
|
847
|
+
* still the right person far more often than no name at all.
|
|
848
|
+
*
|
|
849
|
+
* @param roomId - the room whose roster to read.
|
|
850
|
+
*/
|
|
851
|
+
const loadMembersInto = async (roomId) => {
|
|
852
|
+
try {
|
|
853
|
+
const roster = await listMembers(roomId);
|
|
854
|
+
set({ members: {
|
|
855
|
+
...state.members,
|
|
856
|
+
[roomId]: roster
|
|
857
|
+
} });
|
|
858
|
+
} catch {}
|
|
859
|
+
};
|
|
860
|
+
return {
|
|
861
|
+
getState: () => state,
|
|
862
|
+
subscribe: (listener) => {
|
|
863
|
+
listeners.add(listener);
|
|
864
|
+
return () => {
|
|
865
|
+
listeners.delete(listener);
|
|
866
|
+
};
|
|
867
|
+
},
|
|
868
|
+
refresh: async () => {
|
|
869
|
+
readSelf().then((self) => {
|
|
870
|
+
if (self !== void 0) set({
|
|
871
|
+
selfId: self.id,
|
|
872
|
+
administrator: self.administrator
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
try {
|
|
876
|
+
const rooms = await listRooms();
|
|
877
|
+
set({
|
|
878
|
+
rooms,
|
|
879
|
+
failure: void 0
|
|
880
|
+
});
|
|
881
|
+
for (const room of rooms) {
|
|
882
|
+
if (!room.joined || state.members[room.id] !== void 0) continue;
|
|
883
|
+
loadMembersInto(room.id);
|
|
884
|
+
}
|
|
885
|
+
} catch (error) {
|
|
886
|
+
set({ failure: failureOf(error) });
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
open: (roomId) => {
|
|
890
|
+
set({ activeRoomId: roomId });
|
|
891
|
+
loadMembersInto(roomId);
|
|
892
|
+
},
|
|
893
|
+
discover: async (query) => {
|
|
894
|
+
try {
|
|
895
|
+
set({ discoverable: await listDiscoverableRooms(query) });
|
|
896
|
+
} catch (error) {
|
|
897
|
+
set({
|
|
898
|
+
discoverable: [],
|
|
899
|
+
failure: failureOf(error)
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
join: async (roomId) => {
|
|
904
|
+
await joinRoom(roomId);
|
|
905
|
+
set({
|
|
906
|
+
rooms: await listRooms(),
|
|
907
|
+
discoverable: await listDiscoverableRooms()
|
|
908
|
+
});
|
|
909
|
+
},
|
|
910
|
+
selfId: () => state.selfId,
|
|
911
|
+
administrator: () => state.administrator,
|
|
912
|
+
setFailure: (failure) => set({ failure }),
|
|
913
|
+
setSearch: (searchQuery) => set({ searchQuery }),
|
|
914
|
+
loadMembers: loadMembersInto,
|
|
915
|
+
setPolicy: async (roomId, patch) => {
|
|
916
|
+
await updateRoom(roomId, patch);
|
|
917
|
+
set({ rooms: await listRooms() });
|
|
918
|
+
},
|
|
919
|
+
openCreate: () => set({ pendingCreate: true }),
|
|
920
|
+
closeCreate: () => set({ pendingCreate: false }),
|
|
921
|
+
setDialog: (dialog) => set({ dialog }),
|
|
922
|
+
openMembers: void 0,
|
|
923
|
+
rename: async (roomId, name) => {
|
|
924
|
+
await updateRoom(roomId, { name });
|
|
925
|
+
set({ rooms: await listRooms() });
|
|
926
|
+
},
|
|
927
|
+
leave: async (roomId) => {
|
|
928
|
+
await leaveRoom(roomId);
|
|
929
|
+
set({
|
|
930
|
+
rooms: await listRooms(),
|
|
931
|
+
activeRoomId: void 0
|
|
932
|
+
});
|
|
933
|
+
},
|
|
934
|
+
removeMember: async (roomId, userId) => {
|
|
935
|
+
await removeMember(roomId, userId);
|
|
936
|
+
set({ rooms: await listRooms() });
|
|
937
|
+
},
|
|
938
|
+
dissolve: async (roomId) => {
|
|
939
|
+
await dissolveRoom(roomId);
|
|
940
|
+
set({
|
|
941
|
+
rooms: await listRooms(),
|
|
942
|
+
activeRoomId: void 0
|
|
943
|
+
});
|
|
944
|
+
},
|
|
945
|
+
transfer: async (roomId, userId) => {
|
|
946
|
+
await transferOwnership(roomId, userId);
|
|
947
|
+
set({ rooms: await listRooms() });
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
/** Map a transport failure onto the copy key the surfaces render. */
|
|
952
|
+
const failureOf = (error) => {
|
|
953
|
+
const message = error instanceof Error ? error.message : "";
|
|
954
|
+
if (message === "credentialMissing") return "credentialMissing";
|
|
955
|
+
if (message === "credentialRejected") return "credentialRejected";
|
|
956
|
+
if (message === "notConfigured") return "notConfigured";
|
|
957
|
+
if (message === "ownerCannotLeave") return "ownerCannotLeave";
|
|
958
|
+
return "unreachable";
|
|
959
|
+
};
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/CreateRoomDialog.module.css.mjs
|
|
962
|
+
const css$5 = ".Z3w8WW_body{flex-direction:column;gap:14px;min-width:0;display:flex}.Z3w8WW_field{flex-direction:column;gap:5px;min-width:0;display:flex}.Z3w8WW_label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.Z3w8WW_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}.Z3w8WW_warn{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-warning,#d299221f);color:var(--dsw-alias-state-warning-primary,#d29922);padding:6px 8px;font-size:12px;line-height:17px;display:block}.Z3w8WW_switches{border-top:1px solid var(--dsw-alias-border-l3);flex-direction:column;gap:8px;padding:10px 0 0;display:flex}.Z3w8WW_triggerExtra{flex-wrap:wrap;gap:6px;padding-left:0;display:flex}.Z3w8WW_pill,.Z3w8WW_pillActive{border:1px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border-radius:999px;padding:4px 10px;font-size:12px}.Z3w8WW_pillActive{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-label-primary)}.Z3w8WW_notice{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-bg-layer-2,#80808014);color:var(--dsw-alias-label-secondary);margin:0;padding:8px 10px;font-size:12px;line-height:17px}.Z3w8WW_error{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}.Z3w8WW_footer{justify-content:flex-end;gap:8px;display:flex}.Z3w8WW_policyRow{flex-direction:column;gap:5px;display:flex}.Z3w8WW_memberList{flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;display:flex}.Z3w8WW_memberRow{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;padding:6px 8px;font-size:13px;display:flex}.Z3w8WW_memberRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.Z3w8WW_memberName{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Z3w8WW_memberBadge{background:var(--dsw-alias-bg-layer-2,#8080801f);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:0 6px;font-size:11px;line-height:16px}";
|
|
963
|
+
const tagId$5 = "@sparkelf/dsh-group-chat/CreateRoomDialog.module.css";
|
|
964
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$5) + "]") === null) {
|
|
965
|
+
const tag = document.createElement("style");
|
|
966
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
967
|
+
tag.dataset.pluginCss = tagId$5;
|
|
968
|
+
tag.textContent = css$5;
|
|
969
|
+
document.head.appendChild(tag);
|
|
970
|
+
}
|
|
971
|
+
var CreateRoomDialog_module_css_default = {
|
|
972
|
+
"label": "Z3w8WW_label",
|
|
973
|
+
"pillActive": "Z3w8WW_pillActive",
|
|
974
|
+
"notice": "Z3w8WW_notice",
|
|
975
|
+
"hint": "Z3w8WW_hint",
|
|
976
|
+
"policyRow": "Z3w8WW_policyRow",
|
|
977
|
+
"pill": "Z3w8WW_pill",
|
|
978
|
+
"field": "Z3w8WW_field",
|
|
979
|
+
"memberList": "Z3w8WW_memberList",
|
|
980
|
+
"memberName": "Z3w8WW_memberName",
|
|
981
|
+
"memberBadge": "Z3w8WW_memberBadge",
|
|
982
|
+
"warn": "Z3w8WW_warn",
|
|
983
|
+
"error": "Z3w8WW_error",
|
|
984
|
+
"triggerExtra": "Z3w8WW_triggerExtra",
|
|
985
|
+
"memberRow": "Z3w8WW_memberRow",
|
|
986
|
+
"body": "Z3w8WW_body",
|
|
987
|
+
"footer": "Z3w8WW_footer",
|
|
988
|
+
"switches": "Z3w8WW_switches"
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/client/CreateRoomDialog.tsx
|
|
992
|
+
/**
|
|
993
|
+
* The create-room dialog.
|
|
994
|
+
*
|
|
995
|
+
* Three fields are required because each one decides something the room cannot change
|
|
996
|
+
* later — the name, the working directory, and the agent preset. `cwd` is the real
|
|
997
|
+
* security boundary: every member of the room can ask the agent to modify anything under
|
|
998
|
+
* it, so the field carries a visible warning rather than a hint.
|
|
999
|
+
*
|
|
1000
|
+
* The three policy switches are the room's settings, offered at creation so a room is not
|
|
1001
|
+
* born with defaults the creator would have chosen differently. The notice at the foot is
|
|
1002
|
+
* required wording: these settings control where a room appears, not who may reach it.
|
|
1003
|
+
*
|
|
1004
|
+
* @module @sparkelf/dsh-group-chat/client/CreateRoomDialog
|
|
1005
|
+
*/
|
|
1006
|
+
/**
|
|
1007
|
+
* Render the create-room dialog.
|
|
1008
|
+
* @param props - the copy resolver and shared store whose `pendingCreate` opens this.
|
|
1009
|
+
* @returns the dialog, or null while closed.
|
|
1010
|
+
*/
|
|
1011
|
+
function CreateRoomDialog(props) {
|
|
1012
|
+
const { t, tPreset, store } = props;
|
|
1013
|
+
const open = store.getState().pendingCreate;
|
|
1014
|
+
const [name, setName] = (0, react.useState)("");
|
|
1015
|
+
const [cwd, setCwd] = (0, react.useState)("");
|
|
1016
|
+
const [preset, setPreset] = (0, react.useState)("");
|
|
1017
|
+
const [invitePolicy, setInvitePolicy] = (0, react.useState)("anyone");
|
|
1018
|
+
const [discoverability, setDiscoverability] = (0, react.useState)("discoverable");
|
|
1019
|
+
const [agentTrigger, setAgentTrigger] = (0, react.useState)("mention");
|
|
1020
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1021
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1022
|
+
const [presets, setPresets] = (0, react.useState)([]);
|
|
1023
|
+
const [directories, setDirectories] = (0, react.useState)([]);
|
|
1024
|
+
const [dirPath, setDirPath] = (0, react.useState)("");
|
|
1025
|
+
(0, react.useEffect)(() => {
|
|
1026
|
+
if (!open) return;
|
|
1027
|
+
let cancelled = false;
|
|
1028
|
+
(async () => {
|
|
1029
|
+
try {
|
|
1030
|
+
const options = await readOptions();
|
|
1031
|
+
if (!cancelled) setPresets(options.presets);
|
|
1032
|
+
} catch {
|
|
1033
|
+
if (!cancelled) setPresets([]);
|
|
1034
|
+
}
|
|
1035
|
+
})();
|
|
1036
|
+
return () => {
|
|
1037
|
+
cancelled = true;
|
|
1038
|
+
};
|
|
1039
|
+
}, [open]);
|
|
1040
|
+
(0, react.useEffect)(() => {
|
|
1041
|
+
if (!open) return;
|
|
1042
|
+
let cancelled = false;
|
|
1043
|
+
(async () => {
|
|
1044
|
+
try {
|
|
1045
|
+
const listing = await listDirectories(dirPath.length === 0 ? void 0 : dirPath);
|
|
1046
|
+
if (cancelled) return;
|
|
1047
|
+
const entries = listing.entries.map((entry) => ({
|
|
1048
|
+
id: entry.path,
|
|
1049
|
+
label: entry.name,
|
|
1050
|
+
secondary: entry.path
|
|
1051
|
+
}));
|
|
1052
|
+
setDirectories([
|
|
1053
|
+
{
|
|
1054
|
+
id: listing.path,
|
|
1055
|
+
label: listing.path,
|
|
1056
|
+
secondary: t("pickThisDirectory")
|
|
1057
|
+
},
|
|
1058
|
+
...listing.parent === null ? [] : [{
|
|
1059
|
+
id: listing.parent,
|
|
1060
|
+
label: "..",
|
|
1061
|
+
secondary: listing.parent
|
|
1062
|
+
}],
|
|
1063
|
+
...entries
|
|
1064
|
+
]);
|
|
1065
|
+
} catch {
|
|
1066
|
+
if (!cancelled) setDirectories([]);
|
|
1067
|
+
}
|
|
1068
|
+
})();
|
|
1069
|
+
return () => {
|
|
1070
|
+
cancelled = true;
|
|
1071
|
+
};
|
|
1072
|
+
}, [
|
|
1073
|
+
open,
|
|
1074
|
+
dirPath,
|
|
1075
|
+
t
|
|
1076
|
+
]);
|
|
1077
|
+
(0, react.useEffect)(() => {
|
|
1078
|
+
if (!open) {
|
|
1079
|
+
setName("");
|
|
1080
|
+
setBusy(false);
|
|
1081
|
+
setError(void 0);
|
|
1082
|
+
}
|
|
1083
|
+
}, [open]);
|
|
1084
|
+
const close = () => store.closeCreate();
|
|
1085
|
+
const submit = async () => {
|
|
1086
|
+
if (name.trim().length === 0 || busy) return;
|
|
1087
|
+
setBusy(true);
|
|
1088
|
+
try {
|
|
1089
|
+
const room = await createRoom(name.trim(), {
|
|
1090
|
+
...cwd.trim().length === 0 ? {} : { cwd: cwd.trim() },
|
|
1091
|
+
...preset.trim().length === 0 ? {} : { agentPreset: preset.trim() }
|
|
1092
|
+
});
|
|
1093
|
+
await Promise.all([
|
|
1094
|
+
invitePolicy === "anyone" ? Promise.resolve() : store.setPolicy(room.id, { invitePolicy }),
|
|
1095
|
+
discoverability === "discoverable" ? Promise.resolve() : store.setPolicy(room.id, { discoverability }),
|
|
1096
|
+
agentTrigger === "mention" ? Promise.resolve() : store.setPolicy(room.id, { agentTrigger })
|
|
1097
|
+
]);
|
|
1098
|
+
await store.refresh();
|
|
1099
|
+
store.open(room.id);
|
|
1100
|
+
close();
|
|
1101
|
+
} catch (cause) {
|
|
1102
|
+
setError(failureOf(cause));
|
|
1103
|
+
} finally {
|
|
1104
|
+
setBusy(false);
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1108
|
+
open,
|
|
1109
|
+
onClose: close,
|
|
1110
|
+
title: t("createTitle"),
|
|
1111
|
+
closeLabel: t("cancel"),
|
|
1112
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1113
|
+
className: CreateRoomDialog_module_css_default.footer,
|
|
1114
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1115
|
+
variant: "ghost",
|
|
1116
|
+
size: "md",
|
|
1117
|
+
onClick: close,
|
|
1118
|
+
children: t("cancel")
|
|
1119
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1120
|
+
variant: "primary",
|
|
1121
|
+
size: "md",
|
|
1122
|
+
disabled: busy || name.trim().length === 0,
|
|
1123
|
+
onClick: () => void submit(),
|
|
1124
|
+
children: t("createSubmit")
|
|
1125
|
+
})]
|
|
1126
|
+
}),
|
|
1127
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1128
|
+
className: CreateRoomDialog_module_css_default.body,
|
|
1129
|
+
children: [
|
|
1130
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1131
|
+
className: CreateRoomDialog_module_css_default.field,
|
|
1132
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1133
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1134
|
+
children: t("createName")
|
|
1135
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1136
|
+
value: name,
|
|
1137
|
+
autoFocus: true,
|
|
1138
|
+
onChange: (event) => setName(event.target.value),
|
|
1139
|
+
onKeyDown: (event) => {
|
|
1140
|
+
if (event.key === "Enter") submit();
|
|
1141
|
+
}
|
|
1142
|
+
})]
|
|
1143
|
+
}),
|
|
1144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1145
|
+
className: CreateRoomDialog_module_css_default.field,
|
|
1146
|
+
children: [
|
|
1147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1148
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1149
|
+
children: t("createCwd")
|
|
1150
|
+
}),
|
|
1151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Picker, {
|
|
1152
|
+
value: cwd,
|
|
1153
|
+
options: directories,
|
|
1154
|
+
placeholder: "/workspace",
|
|
1155
|
+
emptyHint: t("optionEmpty"),
|
|
1156
|
+
onQueryChange: (next) => {
|
|
1157
|
+
setCwd(next);
|
|
1158
|
+
},
|
|
1159
|
+
onTypedCommit: (text) => {
|
|
1160
|
+
setCwd(text);
|
|
1161
|
+
setDirPath(text);
|
|
1162
|
+
},
|
|
1163
|
+
typedCommitLabel: (text) => text,
|
|
1164
|
+
onPick: (option) => {
|
|
1165
|
+
setCwd(option.id);
|
|
1166
|
+
setDirPath(option.id);
|
|
1167
|
+
}
|
|
1168
|
+
}),
|
|
1169
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1170
|
+
className: CreateRoomDialog_module_css_default.warn,
|
|
1171
|
+
children: t("createCwdHint")
|
|
1172
|
+
})
|
|
1173
|
+
]
|
|
1174
|
+
}),
|
|
1175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1176
|
+
className: CreateRoomDialog_module_css_default.field,
|
|
1177
|
+
children: [
|
|
1178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1179
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1180
|
+
children: t("createPreset")
|
|
1181
|
+
}),
|
|
1182
|
+
presets.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1183
|
+
className: CreateRoomDialog_module_css_default.hint,
|
|
1184
|
+
children: t("createPresetUnavailable")
|
|
1185
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PresetSelect, {
|
|
1186
|
+
presets,
|
|
1187
|
+
value: preset,
|
|
1188
|
+
t: tPreset,
|
|
1189
|
+
defaultLabel: t("createPresetDefault"),
|
|
1190
|
+
onChange: setPreset
|
|
1191
|
+
}),
|
|
1192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1193
|
+
className: CreateRoomDialog_module_css_default.hint,
|
|
1194
|
+
children: t("createPresetHint")
|
|
1195
|
+
})
|
|
1196
|
+
]
|
|
1197
|
+
}),
|
|
1198
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1199
|
+
className: CreateRoomDialog_module_css_default.switches,
|
|
1200
|
+
children: [
|
|
1201
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
1202
|
+
checked: invitePolicy === "anyone",
|
|
1203
|
+
onChange: (next) => setInvitePolicy(next ? "anyone" : "admin"),
|
|
1204
|
+
label: t("createPolicy")
|
|
1205
|
+
}),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
1207
|
+
checked: discoverability === "discoverable",
|
|
1208
|
+
onChange: (next) => setDiscoverability(next ? "discoverable" : "invite_only"),
|
|
1209
|
+
label: t("createVisibility")
|
|
1210
|
+
}),
|
|
1211
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
1212
|
+
checked: agentTrigger === "mention",
|
|
1213
|
+
onChange: (next) => setAgentTrigger(next ? "mention" : "always"),
|
|
1214
|
+
label: t("createTrigger")
|
|
1215
|
+
}),
|
|
1216
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1217
|
+
className: CreateRoomDialog_module_css_default.triggerExtra,
|
|
1218
|
+
children: [
|
|
1219
|
+
"mention",
|
|
1220
|
+
"always",
|
|
1221
|
+
"never"
|
|
1222
|
+
].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1223
|
+
type: "button",
|
|
1224
|
+
className: agentTrigger === mode ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
|
|
1225
|
+
onClick: () => setAgentTrigger(mode),
|
|
1226
|
+
children: mode === "mention" ? t("triggerMention") : mode === "always" ? t("triggerAlways") : t("triggerNever")
|
|
1227
|
+
}, mode))
|
|
1228
|
+
})
|
|
1229
|
+
]
|
|
1230
|
+
}),
|
|
1231
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1232
|
+
className: CreateRoomDialog_module_css_default.notice,
|
|
1233
|
+
children: t("createNotice")
|
|
1234
|
+
}),
|
|
1235
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1236
|
+
className: CreateRoomDialog_module_css_default.error,
|
|
1237
|
+
children: t(error)
|
|
1238
|
+
})
|
|
1239
|
+
]
|
|
1240
|
+
})
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/InviteDialog.module.css.mjs
|
|
1245
|
+
const css$4 = ".r4uCba_dialog.r4uCba_dialog{width:min(760px,100%);max-height:100%}.r4uCba_content{overscroll-behavior:contain;min-height:0;overflow-y:auto}.r4uCba_searchRow{align-items:center;gap:8px;min-width:0;display:flex;position:relative}.r4uCba_search{flex:1;min-width:0}.r4uCba_spinner{color:var(--dsw-alias-label-tertiary);flex:none;animation:.9s linear infinite r4uCba_inviteSpin}@keyframes r4uCba_inviteSpin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.r4uCba_spinner{animation:none}}.r4uCba_visuallyHidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.r4uCba_field{flex-direction:column;gap:5px;min-width:0;display:flex}.r4uCba_chips{flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none;display:flex}.r4uCba_chip{border:1px solid var(--dsw-alias-border-l3);border-radius:999px;align-items:center;gap:6px;max-width:100%;padding:3px 6px 3px 3px;font-size:12px;line-height:18px;display:inline-flex}.r4uCba_chipName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.r4uCba_chipRemove{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;flex:none;padding:0 2px;font-size:14px;line-height:14px}.r4uCba_chipRemove:hover{color:var(--dsw-alias-label-primary)}.r4uCba_chipRemove:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px;border-radius:var(--dsw-radius-sm)}.r4uCba_list{scrollbar-gutter:stable;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);max-height:min(46vh,420px);margin:0;padding:4px;list-style:none;overflow-y:auto}.r4uCba_list>li+li{margin-top:2px}.r4uCba_row,.r4uCba_rowSelected{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;min-height:38px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;align-items:center;gap:8px;padding:5px 8px;font-size:13px;line-height:18px;display:flex}.r4uCba_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.r4uCba_rowSelected{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-interactive-bg-active)}.r4uCba_rowSelected:hover{background:var(--dsw-alias-interactive-bg-hover-accent)}.r4uCba_row:focus-visible,.r4uCba_rowSelected:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.r4uCba_rowName{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.r4uCba_rowHandle{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px}.r4uCba_check,.r4uCba_checkHidden{width:14px;color:var(--dsw-alias-state-business-primary);flex:none;justify-content:center;align-items:center;display:inline-flex}.r4uCba_checkHidden{visibility:hidden}.r4uCba_note{color:var(--dsw-alias-label-tertiary);margin:0;padding:14px 10px;font-size:12px;line-height:17px}.r4uCba_errorRow{align-items:center;gap:8px;min-width:0;display:flex}.r4uCba_errorRow>span{flex:1;min-width:0}.r4uCba_error{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}";
|
|
1246
|
+
const tagId$4 = "@sparkelf/dsh-group-chat/InviteDialog.module.css";
|
|
1247
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
|
|
1248
|
+
const tag = document.createElement("style");
|
|
1249
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
1250
|
+
tag.dataset.pluginCss = tagId$4;
|
|
1251
|
+
tag.textContent = css$4;
|
|
1252
|
+
document.head.appendChild(tag);
|
|
1253
|
+
}
|
|
1254
|
+
var InviteDialog_module_css_default = {
|
|
1255
|
+
"inviteSpin": "r4uCba_inviteSpin",
|
|
1256
|
+
"rowName": "r4uCba_rowName",
|
|
1257
|
+
"search": "r4uCba_search",
|
|
1258
|
+
"rowSelected": "r4uCba_rowSelected",
|
|
1259
|
+
"note": "r4uCba_note",
|
|
1260
|
+
"chipName": "r4uCba_chipName",
|
|
1261
|
+
"spinner": "r4uCba_spinner",
|
|
1262
|
+
"errorRow": "r4uCba_errorRow",
|
|
1263
|
+
"rowHandle": "r4uCba_rowHandle",
|
|
1264
|
+
"error": "r4uCba_error",
|
|
1265
|
+
"dialog": "r4uCba_dialog",
|
|
1266
|
+
"check": "r4uCba_check",
|
|
1267
|
+
"chip": "r4uCba_chip",
|
|
1268
|
+
"checkHidden": "r4uCba_checkHidden",
|
|
1269
|
+
"chipRemove": "r4uCba_chipRemove",
|
|
1270
|
+
"row": "r4uCba_row",
|
|
1271
|
+
"list": "r4uCba_list",
|
|
1272
|
+
"visuallyHidden": "r4uCba_visuallyHidden",
|
|
1273
|
+
"content": "r4uCba_content",
|
|
1274
|
+
"searchRow": "r4uCba_searchRow",
|
|
1275
|
+
"field": "r4uCba_field",
|
|
1276
|
+
"chips": "r4uCba_chips"
|
|
1277
|
+
};
|
|
1278
|
+
//#endregion
|
|
1279
|
+
//#region src/client/RoomDialogs.tsx
|
|
1280
|
+
/**
|
|
1281
|
+
* The three per-room dialogs: invite, members, and settings.
|
|
1282
|
+
*
|
|
1283
|
+
* They share one module because they are one interaction family — each is opened from a
|
|
1284
|
+
* room row's context menu, each operates on exactly one room, and each writes through the
|
|
1285
|
+
* same store. Splitting them would triple the plumbing without separating anything.
|
|
1286
|
+
*
|
|
1287
|
+
* The settings dialog carries the required notice: its three switches change where a room
|
|
1288
|
+
* appears, not who may reach it. The server enforces permissions; these are preferences.
|
|
1289
|
+
*
|
|
1290
|
+
* @module @sparkelf/dsh-group-chat/client/RoomDialogs
|
|
1291
|
+
*/
|
|
1292
|
+
/**
|
|
1293
|
+
* Render whichever room dialog is open.
|
|
1294
|
+
* @param props - the copy resolver and the store holding the open dialog.
|
|
1295
|
+
* @returns the dialog, or null when none is open.
|
|
1296
|
+
*/
|
|
1297
|
+
function RoomDialogs(props) {
|
|
1298
|
+
const { t, store } = props;
|
|
1299
|
+
const dialog = store.getState().dialog;
|
|
1300
|
+
if (dialog === void 0) return null;
|
|
1301
|
+
const close = () => store.setDialog(void 0);
|
|
1302
|
+
if (dialog.kind === "invite") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InviteDialog, {
|
|
1303
|
+
t,
|
|
1304
|
+
store,
|
|
1305
|
+
roomId: dialog.roomId,
|
|
1306
|
+
onClose: close
|
|
1307
|
+
});
|
|
1308
|
+
if (dialog.kind === "settings") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsDialog, {
|
|
1309
|
+
t,
|
|
1310
|
+
store,
|
|
1311
|
+
roomId: dialog.roomId,
|
|
1312
|
+
onClose: close
|
|
1313
|
+
});
|
|
1314
|
+
if (dialog.kind === "transfer") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TransferDialog, {
|
|
1315
|
+
t,
|
|
1316
|
+
store,
|
|
1317
|
+
roomId: dialog.roomId,
|
|
1318
|
+
onClose: close
|
|
1319
|
+
});
|
|
1320
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MembersDialog, {
|
|
1321
|
+
t,
|
|
1322
|
+
roomId: dialog.roomId,
|
|
1323
|
+
onClose: close
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Add existing DataOps accounts to a room.
|
|
1328
|
+
*
|
|
1329
|
+
* The list is multi-select rather than one-pick-at-a-time: a room is usually started with
|
|
1330
|
+
* several colleagues, and a dialog that closed after each person made that a repeated
|
|
1331
|
+
* errand. Rows are native buttons with `aria-pressed`, so Tab, Enter and Space all work and
|
|
1332
|
+
* a screen reader hears the selected state; ↑/↓ move between rows without leaving the list.
|
|
1333
|
+
*
|
|
1334
|
+
* What is selected lives in `chosen` and not in the search result rows. A query can hide an
|
|
1335
|
+
* already-picked person — the server search returns only what matches — and keeping the two
|
|
1336
|
+
* separate is what stops the selection from silently emptying as the operator narrows the
|
|
1337
|
+
* list.
|
|
1338
|
+
*/
|
|
1339
|
+
function InviteDialog(props) {
|
|
1340
|
+
const { t, store, roomId, onClose } = props;
|
|
1341
|
+
const [candidates, setCandidates] = (0, react.useState)([]);
|
|
1342
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
1343
|
+
const [chosen, setChosen] = (0, react.useState)([]);
|
|
1344
|
+
const [searching, setSearching] = (0, react.useState)(true);
|
|
1345
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1346
|
+
const [searchError, setSearchError] = (0, react.useState)(void 0);
|
|
1347
|
+
const [submitError, setSubmitError] = (0, react.useState)(void 0);
|
|
1348
|
+
const [done, setDone] = (0, react.useState)(false);
|
|
1349
|
+
const [attempt, setAttempt] = (0, react.useState)(0);
|
|
1350
|
+
const listRef = (0, react.useRef)(null);
|
|
1351
|
+
(0, react.useEffect)(() => {
|
|
1352
|
+
let cancelled = false;
|
|
1353
|
+
setSearching(true);
|
|
1354
|
+
const timer = setTimeout(() => {
|
|
1355
|
+
(async () => {
|
|
1356
|
+
try {
|
|
1357
|
+
const next = await searchInviteCandidates(roomId, query);
|
|
1358
|
+
if (!cancelled) {
|
|
1359
|
+
setCandidates(next);
|
|
1360
|
+
setSearchError(void 0);
|
|
1361
|
+
}
|
|
1362
|
+
} catch (cause) {
|
|
1363
|
+
if (!cancelled) setSearchError(failureOf(cause));
|
|
1364
|
+
} finally {
|
|
1365
|
+
if (!cancelled) setSearching(false);
|
|
1366
|
+
}
|
|
1367
|
+
})();
|
|
1368
|
+
}, 220);
|
|
1369
|
+
return () => {
|
|
1370
|
+
cancelled = true;
|
|
1371
|
+
clearTimeout(timer);
|
|
1372
|
+
};
|
|
1373
|
+
}, [
|
|
1374
|
+
roomId,
|
|
1375
|
+
query,
|
|
1376
|
+
attempt
|
|
1377
|
+
]);
|
|
1378
|
+
const toggle = (candidate) => {
|
|
1379
|
+
setDone(false);
|
|
1380
|
+
setSubmitError(void 0);
|
|
1381
|
+
setChosen((current) => toggleChosen(current, candidate));
|
|
1382
|
+
};
|
|
1383
|
+
const submit = async () => {
|
|
1384
|
+
if (chosen.length === 0 || busy) return;
|
|
1385
|
+
setBusy(true);
|
|
1386
|
+
setSubmitError(void 0);
|
|
1387
|
+
try {
|
|
1388
|
+
await inviteMembers(roomId, chosen.map((member) => member.userId));
|
|
1389
|
+
await store.refresh();
|
|
1390
|
+
setChosen([]);
|
|
1391
|
+
setDone(true);
|
|
1392
|
+
if (query.length === 0) setAttempt((current) => current + 1);
|
|
1393
|
+
else setQuery("");
|
|
1394
|
+
} catch (cause) {
|
|
1395
|
+
setSubmitError(failureOf(cause));
|
|
1396
|
+
} finally {
|
|
1397
|
+
setBusy(false);
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
const onListKeyDown = (event) => {
|
|
1401
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
1402
|
+
const rows = [...listRef.current?.querySelectorAll("button[data-candidate]") ?? []];
|
|
1403
|
+
if (rows.length === 0) return;
|
|
1404
|
+
event.preventDefault();
|
|
1405
|
+
const index = rows.indexOf(document.activeElement);
|
|
1406
|
+
const step = event.key === "ArrowDown" ? 1 : -1;
|
|
1407
|
+
rows[index < 0 ? 0 : (index + step + rows.length) % rows.length]?.focus();
|
|
1408
|
+
};
|
|
1409
|
+
const moveToList = () => {
|
|
1410
|
+
listRef.current?.querySelector("button[data-candidate]")?.focus();
|
|
1411
|
+
};
|
|
1412
|
+
const visible = filterCandidates(candidates, query);
|
|
1413
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1414
|
+
open: true,
|
|
1415
|
+
onClose,
|
|
1416
|
+
title: t("inviteTitle"),
|
|
1417
|
+
closeLabel: t("cancel"),
|
|
1418
|
+
className: InviteDialog_module_css_default.dialog,
|
|
1419
|
+
contentClassName: InviteDialog_module_css_default.content,
|
|
1420
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1421
|
+
className: CreateRoomDialog_module_css_default.footer,
|
|
1422
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1423
|
+
variant: "ghost",
|
|
1424
|
+
size: "md",
|
|
1425
|
+
onClick: onClose,
|
|
1426
|
+
children: t("cancel")
|
|
1427
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1428
|
+
variant: "primary",
|
|
1429
|
+
size: "md",
|
|
1430
|
+
disabled: busy || chosen.length === 0,
|
|
1431
|
+
onClick: () => void submit(),
|
|
1432
|
+
children: chosen.length === 0 ? t("inviteSubmit") : t("inviteSubmitCount").replace("{count}", String(chosen.length))
|
|
1433
|
+
})]
|
|
1434
|
+
}),
|
|
1435
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1436
|
+
className: CreateRoomDialog_module_css_default.body,
|
|
1437
|
+
children: [
|
|
1438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1439
|
+
className: InviteDialog_module_css_default.field,
|
|
1440
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1441
|
+
className: InviteDialog_module_css_default.searchRow,
|
|
1442
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1443
|
+
className: InviteDialog_module_css_default.search,
|
|
1444
|
+
value: query,
|
|
1445
|
+
"data-modal-autofocus": true,
|
|
1446
|
+
placeholder: t("invitePlaceholder"),
|
|
1447
|
+
"aria-label": t("inviteSearchLabel"),
|
|
1448
|
+
onChange: (event) => {
|
|
1449
|
+
setQuery(event.target.value);
|
|
1450
|
+
},
|
|
1451
|
+
onKeyDown: (event) => {
|
|
1452
|
+
if (event.key !== "ArrowDown") return;
|
|
1453
|
+
event.preventDefault();
|
|
1454
|
+
moveToList();
|
|
1455
|
+
}
|
|
1456
|
+
}), searching && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLoadingOutlineRegular, {
|
|
1457
|
+
className: InviteDialog_module_css_default.spinner,
|
|
1458
|
+
size: 14
|
|
1459
|
+
})]
|
|
1460
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1461
|
+
className: CreateRoomDialog_module_css_default.hint,
|
|
1462
|
+
children: t("inviteHint")
|
|
1463
|
+
})]
|
|
1464
|
+
}),
|
|
1465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1466
|
+
className: InviteDialog_module_css_default.visuallyHidden,
|
|
1467
|
+
role: "status",
|
|
1468
|
+
children: chosen.length === 0 ? "" : t("inviteSelectedCount").replace("{count}", String(chosen.length))
|
|
1469
|
+
}),
|
|
1470
|
+
chosen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1471
|
+
className: CreateRoomDialog_module_css_default.field,
|
|
1472
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1473
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1474
|
+
children: t("inviteSelectedCount").replace("{count}", String(chosen.length))
|
|
1475
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1476
|
+
className: InviteDialog_module_css_default.chips,
|
|
1477
|
+
children: chosen.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1478
|
+
className: InviteDialog_module_css_default.chip,
|
|
1479
|
+
children: [
|
|
1480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1481
|
+
"aria-hidden": "true",
|
|
1482
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1483
|
+
name: member.displayName || member.username,
|
|
1484
|
+
src: member.avatarUrl,
|
|
1485
|
+
size: "sm"
|
|
1486
|
+
})
|
|
1487
|
+
}),
|
|
1488
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1489
|
+
className: InviteDialog_module_css_default.chipName,
|
|
1490
|
+
children: member.displayName || member.username
|
|
1491
|
+
}),
|
|
1492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1493
|
+
type: "button",
|
|
1494
|
+
className: InviteDialog_module_css_default.chipRemove,
|
|
1495
|
+
"aria-label": `${t("inviteRemove")} ${member.displayName || member.username}`,
|
|
1496
|
+
onClick: () => {
|
|
1497
|
+
setChosen((current) => current.filter((row) => row.userId !== member.userId));
|
|
1498
|
+
},
|
|
1499
|
+
children: "×"
|
|
1500
|
+
})
|
|
1501
|
+
]
|
|
1502
|
+
}, member.userId))
|
|
1503
|
+
})]
|
|
1504
|
+
}),
|
|
1505
|
+
searchError !== void 0 && !searching ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1506
|
+
className: InviteDialog_module_css_default.errorRow,
|
|
1507
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1508
|
+
className: InviteDialog_module_css_default.error,
|
|
1509
|
+
children: t(searchError)
|
|
1510
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1511
|
+
size: "sm",
|
|
1512
|
+
variant: "ghost",
|
|
1513
|
+
onClick: () => {
|
|
1514
|
+
setAttempt((current) => current + 1);
|
|
1515
|
+
},
|
|
1516
|
+
children: t("retry")
|
|
1517
|
+
})]
|
|
1518
|
+
}) : searching && visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1519
|
+
className: InviteDialog_module_css_default.note,
|
|
1520
|
+
children: t("inviteSearching")
|
|
1521
|
+
}) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1522
|
+
className: InviteDialog_module_css_default.note,
|
|
1523
|
+
children: t("inviteEmpty")
|
|
1524
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1525
|
+
ref: listRef,
|
|
1526
|
+
className: InviteDialog_module_css_default.list,
|
|
1527
|
+
onKeyDown: onListKeyDown,
|
|
1528
|
+
children: visible.map((candidate) => {
|
|
1529
|
+
const selected = isChosen(chosen, candidate.userId);
|
|
1530
|
+
const name = candidate.displayName.length > 0 ? candidate.displayName : candidate.username;
|
|
1531
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1532
|
+
type: "button",
|
|
1533
|
+
"data-candidate": true,
|
|
1534
|
+
className: selected ? InviteDialog_module_css_default.rowSelected : InviteDialog_module_css_default.row,
|
|
1535
|
+
"aria-pressed": selected,
|
|
1536
|
+
onClick: () => {
|
|
1537
|
+
toggle(candidate);
|
|
1538
|
+
},
|
|
1539
|
+
children: [
|
|
1540
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1541
|
+
"aria-hidden": "true",
|
|
1542
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1543
|
+
name,
|
|
1544
|
+
src: candidate.avatarUrl,
|
|
1545
|
+
size: "sm"
|
|
1546
|
+
})
|
|
1547
|
+
}),
|
|
1548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1549
|
+
className: InviteDialog_module_css_default.rowName,
|
|
1550
|
+
children: name
|
|
1551
|
+
}),
|
|
1552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1553
|
+
className: InviteDialog_module_css_default.rowHandle,
|
|
1554
|
+
children: candidate.username
|
|
1555
|
+
}),
|
|
1556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1557
|
+
className: selected ? InviteDialog_module_css_default.check : InviteDialog_module_css_default.checkHidden,
|
|
1558
|
+
"aria-hidden": "true",
|
|
1559
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 })
|
|
1560
|
+
})
|
|
1561
|
+
]
|
|
1562
|
+
}) }, candidate.userId);
|
|
1563
|
+
})
|
|
1564
|
+
}),
|
|
1565
|
+
done && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1566
|
+
className: CreateRoomDialog_module_css_default.hint,
|
|
1567
|
+
children: t("saved")
|
|
1568
|
+
}),
|
|
1569
|
+
submitError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1570
|
+
className: InviteDialog_module_css_default.error,
|
|
1571
|
+
children: t(submitError)
|
|
1572
|
+
})
|
|
1573
|
+
]
|
|
1574
|
+
})
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
/** List a room's members. */
|
|
1578
|
+
function MembersDialog(props) {
|
|
1579
|
+
const { t, roomId, onClose } = props;
|
|
1580
|
+
const [members, setMembers] = (0, react.useState)(void 0);
|
|
1581
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1582
|
+
(0, react.useEffect)(() => {
|
|
1583
|
+
let cancelled = false;
|
|
1584
|
+
(async () => {
|
|
1585
|
+
try {
|
|
1586
|
+
const next = await listMembers(roomId);
|
|
1587
|
+
if (!cancelled) setMembers(next);
|
|
1588
|
+
} catch (cause) {
|
|
1589
|
+
if (!cancelled) setError(failureOf(cause));
|
|
1590
|
+
}
|
|
1591
|
+
})();
|
|
1592
|
+
return () => {
|
|
1593
|
+
cancelled = true;
|
|
1594
|
+
};
|
|
1595
|
+
}, [roomId]);
|
|
1596
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1597
|
+
open: true,
|
|
1598
|
+
onClose,
|
|
1599
|
+
title: t("membersTitle"),
|
|
1600
|
+
closeLabel: t("cancel"),
|
|
1601
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1602
|
+
className: CreateRoomDialog_module_css_default.footer,
|
|
1603
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1604
|
+
variant: "ghost",
|
|
1605
|
+
size: "md",
|
|
1606
|
+
onClick: onClose,
|
|
1607
|
+
children: t("cancel")
|
|
1608
|
+
})
|
|
1609
|
+
}),
|
|
1610
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1611
|
+
className: CreateRoomDialog_module_css_default.body,
|
|
1612
|
+
children: [
|
|
1613
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1614
|
+
className: CreateRoomDialog_module_css_default.error,
|
|
1615
|
+
children: t(error)
|
|
1616
|
+
}),
|
|
1617
|
+
error === void 0 && members === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1618
|
+
className: CreateRoomDialog_module_css_default.hint,
|
|
1619
|
+
children: t("loading")
|
|
1620
|
+
}),
|
|
1621
|
+
members !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1622
|
+
className: CreateRoomDialog_module_css_default.memberList,
|
|
1623
|
+
children: members.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
1624
|
+
className: CreateRoomDialog_module_css_default.memberRow,
|
|
1625
|
+
children: [
|
|
1626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1627
|
+
name: member.displayName,
|
|
1628
|
+
src: member.avatarUrl
|
|
1629
|
+
}),
|
|
1630
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1631
|
+
className: CreateRoomDialog_module_css_default.memberName,
|
|
1632
|
+
children: member.displayName
|
|
1633
|
+
}),
|
|
1634
|
+
member.owner && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1635
|
+
className: CreateRoomDialog_module_css_default.memberBadge,
|
|
1636
|
+
children: t("ownerBadge")
|
|
1637
|
+
})
|
|
1638
|
+
]
|
|
1639
|
+
}, member.userId))
|
|
1640
|
+
})
|
|
1641
|
+
]
|
|
1642
|
+
})
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
/** The room's three policy dimensions. */
|
|
1646
|
+
/**
|
|
1647
|
+
* Hand ownership of this room to another member.
|
|
1648
|
+
*
|
|
1649
|
+
* Exists because an owner cannot leave: a room without an owner has nobody who may change its
|
|
1650
|
+
* settings, so the server refuses the leave. Transferring is that way out, and this dialog is
|
|
1651
|
+
* where the owner names the successor.
|
|
1652
|
+
*
|
|
1653
|
+
* Only current members are offered, and the owner is excluded — the server rejects both a
|
|
1654
|
+
* non-member and a self-transfer, so listing them would only invite a refusal.
|
|
1655
|
+
*/
|
|
1656
|
+
function TransferDialog(props) {
|
|
1657
|
+
const { t, store, roomId, onClose } = props;
|
|
1658
|
+
const room = store.getState().rooms?.find((candidate) => candidate.id === roomId);
|
|
1659
|
+
const [members, setMembers] = (0, react.useState)(void 0);
|
|
1660
|
+
const [chosen, setChosen] = (0, react.useState)(void 0);
|
|
1661
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1662
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
1663
|
+
(0, react.useEffect)(() => {
|
|
1664
|
+
let cancelled = false;
|
|
1665
|
+
listMembers(roomId).then((rows) => {
|
|
1666
|
+
if (!cancelled) setMembers(rows);
|
|
1667
|
+
}).catch(() => {
|
|
1668
|
+
if (!cancelled) setMembers([]);
|
|
1669
|
+
});
|
|
1670
|
+
return () => {
|
|
1671
|
+
cancelled = true;
|
|
1672
|
+
};
|
|
1673
|
+
}, [roomId]);
|
|
1674
|
+
const candidates = (members ?? []).filter((member) => member.userId !== store.selfId());
|
|
1675
|
+
const submit = async () => {
|
|
1676
|
+
if (busy || chosen === void 0) return;
|
|
1677
|
+
setBusy(true);
|
|
1678
|
+
setFailure(void 0);
|
|
1679
|
+
try {
|
|
1680
|
+
await store.transfer(roomId, chosen);
|
|
1681
|
+
onClose();
|
|
1682
|
+
} catch (cause) {
|
|
1683
|
+
setFailure(failureOf(cause));
|
|
1684
|
+
} finally {
|
|
1685
|
+
setBusy(false);
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1689
|
+
open: true,
|
|
1690
|
+
onClose: busy ? () => {} : onClose,
|
|
1691
|
+
title: t("menuTransferTitle"),
|
|
1692
|
+
closeLabel: t("cancel"),
|
|
1693
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1694
|
+
className: CreateRoomDialog_module_css_default.footer,
|
|
1695
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1696
|
+
variant: "ghost",
|
|
1697
|
+
size: "md",
|
|
1698
|
+
disabled: busy,
|
|
1699
|
+
onClick: onClose,
|
|
1700
|
+
children: t("cancel")
|
|
1701
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1702
|
+
variant: "primary",
|
|
1703
|
+
size: "md",
|
|
1704
|
+
disabled: busy || chosen === void 0,
|
|
1705
|
+
onClick: () => void submit(),
|
|
1706
|
+
children: t("menuTransferConfirm")
|
|
1707
|
+
})]
|
|
1708
|
+
}),
|
|
1709
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1710
|
+
className: InviteDialog_module_css_default.content,
|
|
1711
|
+
children: [
|
|
1712
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1713
|
+
className: InviteDialog_module_css_default.note,
|
|
1714
|
+
children: t("menuTransferHint")
|
|
1715
|
+
}),
|
|
1716
|
+
room !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1717
|
+
className: InviteDialog_module_css_default.note,
|
|
1718
|
+
children: room.name
|
|
1719
|
+
}),
|
|
1720
|
+
members === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1721
|
+
className: InviteDialog_module_css_default.note,
|
|
1722
|
+
children: "…"
|
|
1723
|
+
}),
|
|
1724
|
+
members !== void 0 && candidates.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1725
|
+
className: InviteDialog_module_css_default.note,
|
|
1726
|
+
children: t("menuTransferEmpty")
|
|
1727
|
+
}),
|
|
1728
|
+
candidates.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
1729
|
+
className: InviteDialog_module_css_default.list,
|
|
1730
|
+
role: "listbox",
|
|
1731
|
+
"aria-label": t("menuTransferTitle"),
|
|
1732
|
+
children: candidates.map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1733
|
+
type: "button",
|
|
1734
|
+
role: "option",
|
|
1735
|
+
"aria-selected": chosen === member.userId,
|
|
1736
|
+
className: chosen === member.userId ? InviteDialog_module_css_default.rowSelected : InviteDialog_module_css_default.row,
|
|
1737
|
+
onClick: () => setChosen(member.userId),
|
|
1738
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
1739
|
+
size: "sm",
|
|
1740
|
+
name: member.displayName,
|
|
1741
|
+
src: member.avatarUrl ?? void 0
|
|
1742
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1743
|
+
className: InviteDialog_module_css_default.rowName,
|
|
1744
|
+
children: member.displayName
|
|
1745
|
+
})]
|
|
1746
|
+
}) }, member.userId))
|
|
1747
|
+
}),
|
|
1748
|
+
failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1749
|
+
className: InviteDialog_module_css_default.error,
|
|
1750
|
+
role: "alert",
|
|
1751
|
+
children: t(failure)
|
|
1752
|
+
})
|
|
1753
|
+
]
|
|
1754
|
+
})
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
function SettingsDialog(props) {
|
|
1758
|
+
const { t, store, roomId, onClose } = props;
|
|
1759
|
+
const room = store.getState().rooms?.find((candidate) => candidate.id === roomId);
|
|
1760
|
+
const [invitePolicy, setInvitePolicy] = (0, react.useState)(room?.invitePolicy ?? "anyone");
|
|
1761
|
+
const [discoverability, setDiscoverability] = (0, react.useState)(room?.discoverability ?? "discoverable");
|
|
1762
|
+
const [agentTrigger, setAgentTrigger] = (0, react.useState)(room?.agentTrigger ?? "mention");
|
|
1763
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1764
|
+
const [error, setError] = (0, react.useState)(void 0);
|
|
1765
|
+
const save = async () => {
|
|
1766
|
+
setBusy(true);
|
|
1767
|
+
try {
|
|
1768
|
+
await updateRoom(roomId, {
|
|
1769
|
+
invitePolicy,
|
|
1770
|
+
discoverability,
|
|
1771
|
+
agentTrigger
|
|
1772
|
+
});
|
|
1773
|
+
await store.refresh();
|
|
1774
|
+
onClose();
|
|
1775
|
+
} catch (cause) {
|
|
1776
|
+
setError(failureOf(cause));
|
|
1777
|
+
} finally {
|
|
1778
|
+
setBusy(false);
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1782
|
+
open: true,
|
|
1783
|
+
onClose,
|
|
1784
|
+
title: t("settingsTitle"),
|
|
1785
|
+
closeLabel: t("cancel"),
|
|
1786
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1787
|
+
className: CreateRoomDialog_module_css_default.footer,
|
|
1788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1789
|
+
variant: "ghost",
|
|
1790
|
+
size: "md",
|
|
1791
|
+
onClick: onClose,
|
|
1792
|
+
children: t("cancel")
|
|
1793
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1794
|
+
variant: "primary",
|
|
1795
|
+
size: "md",
|
|
1796
|
+
disabled: busy,
|
|
1797
|
+
onClick: () => void save(),
|
|
1798
|
+
children: t("save")
|
|
1799
|
+
})]
|
|
1800
|
+
}),
|
|
1801
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1802
|
+
className: CreateRoomDialog_module_css_default.body,
|
|
1803
|
+
children: [
|
|
1804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1805
|
+
className: CreateRoomDialog_module_css_default.switches,
|
|
1806
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicyRow, {
|
|
1807
|
+
t,
|
|
1808
|
+
label: t("createPolicy"),
|
|
1809
|
+
value: invitePolicy === "anyone",
|
|
1810
|
+
onChange: (next) => setInvitePolicy(next ? "anyone" : "admin"),
|
|
1811
|
+
onLabel: t("policyAnyone"),
|
|
1812
|
+
offLabel: t("policyAdmin")
|
|
1813
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PolicyRow, {
|
|
1814
|
+
t,
|
|
1815
|
+
label: t("createVisibility"),
|
|
1816
|
+
value: discoverability === "discoverable",
|
|
1817
|
+
onChange: (next) => setDiscoverability(next ? "discoverable" : "invite_only"),
|
|
1818
|
+
onLabel: t("discoverable"),
|
|
1819
|
+
offLabel: t("inviteOnly")
|
|
1820
|
+
})]
|
|
1821
|
+
}),
|
|
1822
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1823
|
+
className: CreateRoomDialog_module_css_default.switches,
|
|
1824
|
+
style: {
|
|
1825
|
+
borderTop: "none",
|
|
1826
|
+
paddingTop: 0
|
|
1827
|
+
},
|
|
1828
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1829
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1830
|
+
children: t("createTrigger")
|
|
1831
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1832
|
+
className: CreateRoomDialog_module_css_default.triggerExtra,
|
|
1833
|
+
children: [
|
|
1834
|
+
"mention",
|
|
1835
|
+
"always",
|
|
1836
|
+
"never"
|
|
1837
|
+
].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1838
|
+
type: "button",
|
|
1839
|
+
className: agentTrigger === mode ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
|
|
1840
|
+
onClick: () => setAgentTrigger(mode),
|
|
1841
|
+
children: mode === "mention" ? t("triggerMention") : mode === "always" ? t("triggerAlways") : t("triggerNever")
|
|
1842
|
+
}, mode))
|
|
1843
|
+
})]
|
|
1844
|
+
}),
|
|
1845
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1846
|
+
className: CreateRoomDialog_module_css_default.notice,
|
|
1847
|
+
children: t("createNotice")
|
|
1848
|
+
}),
|
|
1849
|
+
error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1850
|
+
className: CreateRoomDialog_module_css_default.error,
|
|
1851
|
+
children: t(error)
|
|
1852
|
+
})
|
|
1853
|
+
]
|
|
1854
|
+
})
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
/** One two-state policy row, labelled with what each side means. */
|
|
1858
|
+
function PolicyRow(props) {
|
|
1859
|
+
const { label, value, onChange, onLabel, offLabel } = props;
|
|
1860
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1861
|
+
className: CreateRoomDialog_module_css_default.policyRow,
|
|
1862
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1863
|
+
className: CreateRoomDialog_module_css_default.label,
|
|
1864
|
+
children: label
|
|
1865
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1866
|
+
className: CreateRoomDialog_module_css_default.triggerExtra,
|
|
1867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1868
|
+
type: "button",
|
|
1869
|
+
className: value ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
|
|
1870
|
+
onClick: () => onChange(true),
|
|
1871
|
+
children: onLabel
|
|
1872
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1873
|
+
type: "button",
|
|
1874
|
+
className: !value ? CreateRoomDialog_module_css_default.pillActive : CreateRoomDialog_module_css_default.pill,
|
|
1875
|
+
onClick: () => onChange(false),
|
|
1876
|
+
children: offLabel
|
|
1877
|
+
})]
|
|
1878
|
+
})]
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
//#endregion
|
|
1882
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/SidebarRooms.module.css.mjs
|
|
1883
|
+
const css$3 = ".s0QgKq_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding,12px);--dsh-session-list-scrollbar-width:5px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;flex-direction:column;flex:auto;width:100%;min-width:0;min-height:0;max-height:20vh;display:flex;position:relative}.s0QgKq_resizer{z-index:1;cursor:row-resize;touch-action:none;background:0 0;height:6px;position:absolute;top:-3px;left:0;right:0}.s0QgKq_resizer:after{content:\"\";background:var(--dsw-alias-border-l1,var(--dsw-alias-separator-primary));opacity:0;border-radius:1px;height:1px;transition:opacity .12s;position:absolute;top:3px;left:8px;right:8px}.s0QgKq_root:hover .s0QgKq_resizer:after,.s0QgKq_resizer:focus-visible:after{opacity:1}.s0QgKq_resizer:focus-visible{outline:none}.s0QgKq_resizer:focus-visible:after{background:var(--dsw-alias-brand-primary,var(--dsw-alias-label-primary));height:2px}.s0QgKq_sectionHeader{box-sizing:border-box;border-radius:var(--dsw-radius-md);height:36px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;margin-right:-4px;padding-left:4px;display:flex;overflow:hidden}.s0QgKq_sectionLabel{white-space:nowrap;flex:none;min-width:0;max-width:45%;line-height:20px;overflow:hidden}.s0QgKq_searchSlot{box-sizing:border-box;flex:1;align-items:center;min-width:0;max-width:28px;margin-left:auto;padding-left:0;display:flex}.s0QgKq_searchSlotExpanded{box-sizing:border-box;flex:1;align-items:center;min-width:0;max-width:100%;margin-left:auto;display:flex}.s0QgKq_search,.s0QgKq_searchExpanded{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;height:28px;color:var(--dsw-alias-label-secondary);cursor:text;background:0 0;border:none;flex:none;align-items:center;margin:0;padding:0;display:flex;overflow:hidden}.s0QgKq_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);margin-inline:-2px;padding:0 4px 0 0}.s0QgKq_iconButton{border-radius:var(--dsw-radius-sm);width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_iconButton:focus-visible,.s0QgKq_searchButton:focus-visible,.s0QgKq_clearButton:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-2px}.s0QgKq_searchButton{border-radius:var(--dsw-radius-sm);width:28px;height:28px;color:inherit;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_searchExpanded .s0QgKq_searchButton{width:28px;height:30px}.s0QgKq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_searchExpanded .s0QgKq_searchButton:hover{background:0 0}.s0QgKq_searchInput{width:0;min-width:0;color:var(--dsw-alias-label-primary);opacity:0;pointer-events:none;transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;padding:0;font-size:13px;line-height:18px}.s0QgKq_searchExpanded .s0QgKq_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.s0QgKq_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.s0QgKq_clearButton{border-radius:var(--dsw-radius-sm);width:24px;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.s0QgKq_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_headerActions{flex:none;align-items:center;gap:4px;display:flex}.s0QgKq_viewWrap{display:flex;position:relative}.s0QgKq_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;position:relative}.s0QgKq_list{min-height:0;max-height:100%;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;padding-bottom:8px;overflow-y:auto}.s0QgKq_flatList{margin:0;padding:0;list-style:none}.s0QgKq_flatList>*+*{margin-top:2px}.s0QgKq_sessionRow,.s0QgKq_sessionRowSelected{box-sizing:border-box;border-radius:var(--dsw-radius-sm);width:100%;height:32px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:0 8px;font-size:14px;display:flex}.s0QgKq_sessionRow:hover,.s0QgKq_sessionRowSelected{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_rowIcon{width:16px;color:var(--dsw-alias-label-secondary);flex:none;justify-content:center;align-items:center;display:flex}.s0QgKq_title{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.s0QgKq_time{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:12px;line-height:17px}.s0QgKq_unread{background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-interactive-bg-hover));min-width:18px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-align:center;border-radius:9px;flex:none;padding:0 5px;font-size:11px;font-weight:600;line-height:17px}.s0QgKq_unreadMentioned{background:var(--dsw-alias-state-business-primary);min-width:18px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;text-align:center;border-radius:9px;flex:none;padding:0 5px;font-size:11px;font-weight:600;line-height:17px}.s0QgKq_empty{color:var(--dsw-alias-label-tertiary);margin:0;padding:16px 12px;font-size:13px}.s0QgKq_emptyState{color:var(--dsw-alias-label-tertiary);text-align:center;flex-direction:column;align-items:center;gap:8px;margin-top:80px;padding:0 12px;font-size:13px;line-height:20px;display:flex}.s0QgKq_emptyState>svg{color:var(--dsw-alias-label-caption);margin-bottom:4px}.s0QgKq_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}.s0QgKq_menuLayer{z-index:60;max-height:calc(100vh - 16px);position:fixed;overflow-y:auto}.s0QgKq_menuInline{z-index:30;position:absolute;top:32px;right:0}.s0QgKq_menu{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1,var(--dsw-specific-sidebar-fill));min-width:200px;padding:4px;position:relative;box-shadow:0 6px 20px #0000002e}.s0QgKq_menuHeading{color:var(--dsw-alias-label-tertiary);padding:4px 8px 2px;font-size:11px;line-height:16px;display:block}.s0QgKq_menuItem{border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;align-items:center;gap:8px;padding:6px 8px;font-size:13px;display:flex}.s0QgKq_menuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_check{color:var(--dsw-alias-state-business-primary);margin-left:auto}.s0QgKq_visuallyHidden{clip-path:inset(50%);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.s0QgKq_rowWrap{position:relative}.s0QgKq_menuRename{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1,var(--dsw-specific-sidebar-fill));min-width:240px;padding:8px;box-shadow:0 6px 20px #0000002e}.s0QgKq_menuRename .s0QgKq_menuHeading{padding:0 0 6px}.s0QgKq_menuInput{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-sm);appearance:none;background:var(--dsw-alias-bg-base);width:100%;color:var(--dsw-alias-label-primary);font:inherit;outline:none;margin:2px 0 6px;padding:6px 8px;font-size:13px;line-height:20px}.s0QgKq_menuInput:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.s0QgKq_menuInput::placeholder{color:var(--dsw-alias-label-tertiary)}.s0QgKq_menuActions{justify-content:flex-end;gap:6px;padding-top:2px;display:flex}.s0QgKq_menuConfirm{box-sizing:border-box;border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font:inherit;cursor:pointer;border:1px solid #0000;padding:5px 12px;font-size:12px;line-height:18px}.s0QgKq_menuConfirm:hover:not(:disabled){filter:brightness(1.06)}.s0QgKq_menuConfirm:disabled{opacity:.5;cursor:default}.s0QgKq_menuCancel{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;padding:5px 12px;font-size:12px;line-height:18px}.s0QgKq_menuCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_menuItem:disabled{opacity:.5;cursor:default}.s0QgKq_discoverSection{border-top:1px solid var(--dsw-alias-border-l3);margin-top:8px;padding-top:8px}.s0QgKq_discoverHeading{color:var(--dsw-alias-label-tertiary);margin:0 0 4px;padding:0 12px;font-size:11px;line-height:16px}.s0QgKq_discoverRow{border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-primary);align-items:center;gap:8px;padding:4px 12px;font-size:13px;line-height:18px;display:flex}.s0QgKq_discoverRow:hover{background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_discoverName{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.s0QgKq_discoverMeta{color:var(--dsw-alias-label-tertiary);flex:none;font-size:11px}.s0QgKq_joinButton{border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-business-primary);font:inherit;cursor:pointer;background:0 0;flex:none;padding:2px 10px;font-size:12px}.s0QgKq_joinButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.s0QgKq_joinButton:disabled{opacity:.5;cursor:default}.s0QgKq_leaveBody{flex-direction:column;gap:8px;min-width:0;display:flex}.s0QgKq_leaveCopy{color:var(--dsw-alias-label-primary);margin:0;font-size:14px;line-height:22px}.s0QgKq_leaveRoomName{color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;margin:0;font-size:14px;line-height:22px}.s0QgKq_leaveRoomName strong{font-weight:600}.s0QgKq_leaveError{color:var(--dsw-alias-state-error-primary,#f85149);margin:0;font-size:12px;line-height:17px}.s0QgKq_footer{justify-content:flex-end;gap:8px;display:flex}.s0QgKq_leaveConfirmDanger{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary)}.s0QgKq_leaveConfirmDanger.s0QgKq_leaveConfirmDanger{color:var(--dsw-alias-label-primary)}.s0QgKq_leaveConfirmDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);filter:brightness(1.08)}.s0QgKq_leaveConfirmDanger:disabled{opacity:.5}";
|
|
1884
|
+
const tagId$3 = "@sparkelf/dsh-group-chat/SidebarRooms.module.css";
|
|
1885
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
1886
|
+
const tag = document.createElement("style");
|
|
1887
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
1888
|
+
tag.dataset.pluginCss = tagId$3;
|
|
1889
|
+
tag.textContent = css$3;
|
|
1890
|
+
document.head.appendChild(tag);
|
|
1891
|
+
}
|
|
1892
|
+
var SidebarRooms_module_css_default = {
|
|
1893
|
+
"rowIcon": "s0QgKq_rowIcon",
|
|
1894
|
+
"unread": "s0QgKq_unread",
|
|
1895
|
+
"headerActions": "s0QgKq_headerActions",
|
|
1896
|
+
"leaveRoomName": "s0QgKq_leaveRoomName",
|
|
1897
|
+
"menu": "s0QgKq_menu",
|
|
1898
|
+
"menuRename": "s0QgKq_menuRename",
|
|
1899
|
+
"searchSlotExpanded": "s0QgKq_searchSlotExpanded",
|
|
1900
|
+
"rowWrap": "s0QgKq_rowWrap",
|
|
1901
|
+
"menuConfirm": "s0QgKq_menuConfirm",
|
|
1902
|
+
"viewWrap": "s0QgKq_viewWrap",
|
|
1903
|
+
"joinButton": "s0QgKq_joinButton",
|
|
1904
|
+
"listArea": "s0QgKq_listArea",
|
|
1905
|
+
"sessionRowSelected": "s0QgKq_sessionRowSelected",
|
|
1906
|
+
"fade": "s0QgKq_fade",
|
|
1907
|
+
"sectionLabel": "s0QgKq_sectionLabel",
|
|
1908
|
+
"discoverHeading": "s0QgKq_discoverHeading",
|
|
1909
|
+
"check": "s0QgKq_check",
|
|
1910
|
+
"discoverName": "s0QgKq_discoverName",
|
|
1911
|
+
"iconButton": "s0QgKq_iconButton",
|
|
1912
|
+
"menuLayer": "s0QgKq_menuLayer",
|
|
1913
|
+
"leaveCopy": "s0QgKq_leaveCopy",
|
|
1914
|
+
"empty": "s0QgKq_empty",
|
|
1915
|
+
"emptyState": "s0QgKq_emptyState",
|
|
1916
|
+
"clearButton": "s0QgKq_clearButton",
|
|
1917
|
+
"discoverRow": "s0QgKq_discoverRow",
|
|
1918
|
+
"leaveConfirmDanger": "s0QgKq_leaveConfirmDanger",
|
|
1919
|
+
"unreadMentioned": "s0QgKq_unreadMentioned",
|
|
1920
|
+
"searchButton": "s0QgKq_searchButton",
|
|
1921
|
+
"search": "s0QgKq_search",
|
|
1922
|
+
"searchInput": "s0QgKq_searchInput",
|
|
1923
|
+
"discoverSection": "s0QgKq_discoverSection",
|
|
1924
|
+
"sessionRow": "s0QgKq_sessionRow",
|
|
1925
|
+
"menuItem": "s0QgKq_menuItem",
|
|
1926
|
+
"menuCancel": "s0QgKq_menuCancel",
|
|
1927
|
+
"title": "s0QgKq_title",
|
|
1928
|
+
"searchSlot": "s0QgKq_searchSlot",
|
|
1929
|
+
"resizer": "s0QgKq_resizer",
|
|
1930
|
+
"flatList": "s0QgKq_flatList",
|
|
1931
|
+
"root": "s0QgKq_root",
|
|
1932
|
+
"time": "s0QgKq_time",
|
|
1933
|
+
"menuInline": "s0QgKq_menuInline",
|
|
1934
|
+
"menuHeading": "s0QgKq_menuHeading",
|
|
1935
|
+
"sectionHeader": "s0QgKq_sectionHeader",
|
|
1936
|
+
"menuInput": "s0QgKq_menuInput",
|
|
1937
|
+
"list": "s0QgKq_list",
|
|
1938
|
+
"searchExpanded": "s0QgKq_searchExpanded",
|
|
1939
|
+
"visuallyHidden": "s0QgKq_visuallyHidden",
|
|
1940
|
+
"leaveBody": "s0QgKq_leaveBody",
|
|
1941
|
+
"leaveError": "s0QgKq_leaveError",
|
|
1942
|
+
"footer": "s0QgKq_footer",
|
|
1943
|
+
"menuActions": "s0QgKq_menuActions",
|
|
1944
|
+
"discoverMeta": "s0QgKq_discoverMeta"
|
|
1945
|
+
};
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/client/SidebarRooms.tsx
|
|
1948
|
+
/**
|
|
1949
|
+
* The group-chat region of the left sidebar.
|
|
1950
|
+
*
|
|
1951
|
+
* Structured after `ui-workspace`'s browsing region so the two read as the same design
|
|
1952
|
+
* system rather than a lookalike: a 36px header holding a 14px section label, a
|
|
1953
|
+
* collapsible search slot, and the trailing icon actions (view options, new room), then
|
|
1954
|
+
* a flat list of rows. Sizes, tokens, and icon set are the ones that package uses — the
|
|
1955
|
+
* group-chat region is the workspace region with the grouping removed, because a room is
|
|
1956
|
+
* 1:1 with its session while a workspace is 1:N.
|
|
1957
|
+
*
|
|
1958
|
+
* Registered into `sidebar.footer.action`, the official list seat at the sidebar foot. The
|
|
1959
|
+
* shell renders it inside `footerActions` (full width, `flex: none`), under
|
|
1960
|
+
* `sidebar.workspaces` and above Settings.
|
|
1961
|
+
*
|
|
1962
|
+
* @module @sparkelf/dsh-group-chat/client/SidebarRooms
|
|
1963
|
+
*/
|
|
1964
|
+
/**
|
|
1965
|
+
* Height bounds for the region, in pixels.
|
|
1966
|
+
*
|
|
1967
|
+
* The floor keeps a room row and the header visible; the ceiling keeps the region from
|
|
1968
|
+
* crowding out the workspace list above it, which is the same reason the stylesheet capped it
|
|
1969
|
+
* at `46vh` before this was adjustable. The host clamps to the same pair, so a stored value
|
|
1970
|
+
* cannot arrive outside the range the region can draw.
|
|
1971
|
+
*/
|
|
1972
|
+
const HEIGHT_MIN = 120;
|
|
1973
|
+
const HEIGHT_MAX = 1200;
|
|
1974
|
+
/** Clamps a requested height into the range the region can draw. */
|
|
1975
|
+
const clampHeight = (value) => Math.min(HEIGHT_MAX, Math.max(HEIGHT_MIN, Math.round(value)));
|
|
1976
|
+
/** How often the list re-reads, so a peer's new room appears without a reload. */
|
|
1977
|
+
const REFRESH_MS = 1e4;
|
|
1978
|
+
/**
|
|
1979
|
+
* Render the sidebar's group-chat region.
|
|
1980
|
+
* @param props - the injected copy resolver and the shared room store.
|
|
1981
|
+
* @returns the section header plus the flat room list.
|
|
1982
|
+
*/
|
|
1983
|
+
function SidebarRooms(props) {
|
|
1984
|
+
const { t, tPreset, store } = props;
|
|
1985
|
+
const { rooms, activeRoomId, failure, searchQuery, discoverable } = (0, react.useSyncExternalStore)(store.subscribe, store.getState, store.getState);
|
|
1986
|
+
const [searchExpanded, setSearchExpanded] = (0, react.useState)(false);
|
|
1987
|
+
const [viewOpen, setViewOpen] = (0, react.useState)(false);
|
|
1988
|
+
const [orderBy, setOrderBy] = (0, react.useState)("updated");
|
|
1989
|
+
const [archivedFilter, setArchivedFilter] = (0, react.useState)("default");
|
|
1990
|
+
const [joining, setJoining] = (0, react.useState)(void 0);
|
|
1991
|
+
const searchInput = (0, react.useRef)(null);
|
|
1992
|
+
/**
|
|
1993
|
+
* Height of this region, in pixels, once the user has dragged it.
|
|
1994
|
+
*
|
|
1995
|
+
* `undefined` until either the saved value arrives or a drag happens: the region then keeps
|
|
1996
|
+
* the stylesheet's `max-height` and shrinks to its content, which is what every workspace
|
|
1997
|
+
* looked like before this existed. Storing a number here switches it to a fixed height.
|
|
1998
|
+
*/
|
|
1999
|
+
const [heightPx, setHeightPx] = (0, react.useState)(void 0);
|
|
2000
|
+
/** The height being dragged to; `undefined` when no drag is in progress. */
|
|
2001
|
+
const [draggingPx, setDraggingPx] = (0, react.useState)(void 0);
|
|
2002
|
+
const dragStart = (0, react.useRef)(null);
|
|
2003
|
+
(0, react.useEffect)(() => {
|
|
2004
|
+
store.refresh();
|
|
2005
|
+
}, [store]);
|
|
2006
|
+
(0, react.useEffect)(() => {
|
|
2007
|
+
let live = true;
|
|
2008
|
+
readPreferences().then((saved) => {
|
|
2009
|
+
if (!live || saved.sidebarHeightPx === void 0) return;
|
|
2010
|
+
setHeightPx(saved.sidebarHeightPx);
|
|
2011
|
+
});
|
|
2012
|
+
return () => {
|
|
2013
|
+
live = false;
|
|
2014
|
+
};
|
|
2015
|
+
}, []);
|
|
2016
|
+
(0, react.useEffect)(() => {
|
|
2017
|
+
const timer = setInterval(() => {
|
|
2018
|
+
store.refresh();
|
|
2019
|
+
}, REFRESH_MS);
|
|
2020
|
+
return () => clearInterval(timer);
|
|
2021
|
+
}, [store]);
|
|
2022
|
+
/**
|
|
2023
|
+
* Start a height drag from the grip on the region's top edge.
|
|
2024
|
+
*
|
|
2025
|
+
* The delta is inverted because the grip sits above the region: dragging up (a smaller `y`)
|
|
2026
|
+
* makes the region taller, which is the same direction the right sidebar's grip works in.
|
|
2027
|
+
* Listeners go on the document so the pointer may leave the 4px grip — and the window —
|
|
2028
|
+
* without dropping the drag halfway.
|
|
2029
|
+
*/
|
|
2030
|
+
const beginResize = (event) => {
|
|
2031
|
+
event.preventDefault();
|
|
2032
|
+
const section = event.currentTarget.parentElement;
|
|
2033
|
+
if (section === null) return;
|
|
2034
|
+
dragStart.current = {
|
|
2035
|
+
y: event.clientY,
|
|
2036
|
+
height: section.getBoundingClientRect().height
|
|
2037
|
+
};
|
|
2038
|
+
setDraggingPx(dragStart.current.height);
|
|
2039
|
+
const onMove = (move) => {
|
|
2040
|
+
const start = dragStart.current;
|
|
2041
|
+
if (start === null) return;
|
|
2042
|
+
setDraggingPx(clampHeight(start.height + (start.y - move.clientY)));
|
|
2043
|
+
};
|
|
2044
|
+
const onUp = () => {
|
|
2045
|
+
document.removeEventListener("pointermove", onMove);
|
|
2046
|
+
document.removeEventListener("pointerup", onUp);
|
|
2047
|
+
document.removeEventListener("pointercancel", onUp);
|
|
2048
|
+
dragStart.current = null;
|
|
2049
|
+
setDraggingPx((settled) => {
|
|
2050
|
+
if (settled !== void 0) {
|
|
2051
|
+
setHeightPx(settled);
|
|
2052
|
+
savePreferences({ sidebarHeightPx: settled }).catch(() => void 0);
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
};
|
|
2056
|
+
document.addEventListener("pointermove", onMove);
|
|
2057
|
+
document.addEventListener("pointerup", onUp);
|
|
2058
|
+
document.addEventListener("pointercancel", onUp);
|
|
2059
|
+
};
|
|
2060
|
+
(0, react.useEffect)(() => {
|
|
2061
|
+
if (!searchExpanded) return;
|
|
2062
|
+
const onDown = (event) => {
|
|
2063
|
+
if (event.target?.closest(`.${SidebarRooms_module_css_default.search}`) === null) {
|
|
2064
|
+
if (searchQuery.length === 0) setSearchExpanded(false);
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
document.addEventListener("mousedown", onDown);
|
|
2068
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
2069
|
+
}, [searchExpanded, searchQuery.length]);
|
|
2070
|
+
const query = searchQuery.trim().toLowerCase();
|
|
2071
|
+
const visible = (rooms ?? []).filter((room) => archivedFilter === "all" || room.joined).filter((room) => query.length === 0 || room.name.toLowerCase().includes(query)).slice().sort((a, b) => orderBy === "manual" ? 0 : Date.parse(b.updatedAt || b.createdAt) - Date.parse(a.updatedAt || a.createdAt));
|
|
2072
|
+
const onPickOrder = (0, react.useCallback)(async (mode) => {
|
|
2073
|
+
setOrderBy(mode);
|
|
2074
|
+
setViewOpen(false);
|
|
2075
|
+
}, []);
|
|
2076
|
+
(0, react.useEffect)(() => {
|
|
2077
|
+
const keyword = searchQuery.trim();
|
|
2078
|
+
if (keyword.length === 0) return;
|
|
2079
|
+
let cancelled = false;
|
|
2080
|
+
const timer = setTimeout(() => {
|
|
2081
|
+
if (!cancelled) store.discover(keyword);
|
|
2082
|
+
}, 300);
|
|
2083
|
+
return () => {
|
|
2084
|
+
cancelled = true;
|
|
2085
|
+
clearTimeout(timer);
|
|
2086
|
+
};
|
|
2087
|
+
}, [searchQuery, store]);
|
|
2088
|
+
const joinable = joinableRooms(discoverable ?? [], new Set((rooms ?? []).map((room) => room.id)));
|
|
2089
|
+
const onJoin = async (roomId) => {
|
|
2090
|
+
setJoining(roomId);
|
|
2091
|
+
try {
|
|
2092
|
+
await store.join(roomId);
|
|
2093
|
+
store.open(roomId);
|
|
2094
|
+
store.setSearch("");
|
|
2095
|
+
setSearchExpanded(false);
|
|
2096
|
+
} finally {
|
|
2097
|
+
setJoining(void 0);
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2101
|
+
className: SidebarRooms_module_css_default.root,
|
|
2102
|
+
"aria-label": t("title"),
|
|
2103
|
+
style: {
|
|
2104
|
+
height: `${String(draggingPx ?? heightPx ?? HEIGHT_MIN)}px`,
|
|
2105
|
+
maxHeight: "none"
|
|
2106
|
+
},
|
|
2107
|
+
children: [
|
|
2108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2109
|
+
className: SidebarRooms_module_css_default.resizer,
|
|
2110
|
+
role: "separator",
|
|
2111
|
+
"aria-orientation": "horizontal",
|
|
2112
|
+
"aria-label": t("resizeRegion"),
|
|
2113
|
+
"aria-valuenow": Math.round(draggingPx ?? heightPx ?? 0) || void 0,
|
|
2114
|
+
tabIndex: 0,
|
|
2115
|
+
onPointerDown: beginResize,
|
|
2116
|
+
onKeyDown: (event) => {
|
|
2117
|
+
const step = event.shiftKey ? 40 : 10;
|
|
2118
|
+
const grow = event.key === "ArrowUp" || event.key === "ArrowLeft";
|
|
2119
|
+
const shrink = event.key === "ArrowDown" || event.key === "ArrowRight";
|
|
2120
|
+
if (!grow && !shrink) return;
|
|
2121
|
+
event.preventDefault();
|
|
2122
|
+
event.currentTarget.parentElement;
|
|
2123
|
+
const next = clampHeight((heightPx ?? HEIGHT_MIN) + (grow ? step : -step));
|
|
2124
|
+
setHeightPx(next);
|
|
2125
|
+
savePreferences({ sidebarHeightPx: next }).catch(() => void 0);
|
|
2126
|
+
}
|
|
2127
|
+
}),
|
|
2128
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2129
|
+
className: SidebarRooms_module_css_default.sectionHeader,
|
|
2130
|
+
children: [
|
|
2131
|
+
!searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2132
|
+
className: SidebarRooms_module_css_default.sectionLabel,
|
|
2133
|
+
children: t("title")
|
|
2134
|
+
}),
|
|
2135
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2136
|
+
className: searchExpanded ? SidebarRooms_module_css_default.searchSlotExpanded : SidebarRooms_module_css_default.searchSlot,
|
|
2137
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2138
|
+
className: searchExpanded ? SidebarRooms_module_css_default.searchExpanded : SidebarRooms_module_css_default.search,
|
|
2139
|
+
onClick: () => {
|
|
2140
|
+
setSearchExpanded(true);
|
|
2141
|
+
searchInput.current?.focus();
|
|
2142
|
+
},
|
|
2143
|
+
children: [
|
|
2144
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2145
|
+
label: t("search"),
|
|
2146
|
+
side: "bottom",
|
|
2147
|
+
delayMs: 500,
|
|
2148
|
+
disabled: searchExpanded,
|
|
2149
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2150
|
+
type: "button",
|
|
2151
|
+
className: SidebarRooms_module_css_default.searchButton,
|
|
2152
|
+
"aria-label": t("search"),
|
|
2153
|
+
"aria-expanded": searchExpanded,
|
|
2154
|
+
onClick: (event) => {
|
|
2155
|
+
event.stopPropagation();
|
|
2156
|
+
setSearchExpanded((v) => !v);
|
|
2157
|
+
},
|
|
2158
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: searchExpanded ? 11 : 14 })
|
|
2159
|
+
})
|
|
2160
|
+
}),
|
|
2161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2162
|
+
ref: searchInput,
|
|
2163
|
+
className: SidebarRooms_module_css_default.searchInput,
|
|
2164
|
+
type: "text",
|
|
2165
|
+
placeholder: t("searchPlaceholder"),
|
|
2166
|
+
value: searchQuery,
|
|
2167
|
+
tabIndex: searchExpanded ? 0 : -1,
|
|
2168
|
+
onChange: (event) => store.setSearch(event.target.value),
|
|
2169
|
+
onKeyDown: (event) => {
|
|
2170
|
+
if (event.key !== "Escape") return;
|
|
2171
|
+
store.setSearch("");
|
|
2172
|
+
setSearchExpanded(false);
|
|
2173
|
+
}
|
|
2174
|
+
}),
|
|
2175
|
+
searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2176
|
+
type: "button",
|
|
2177
|
+
className: SidebarRooms_module_css_default.clearButton,
|
|
2178
|
+
"aria-label": t("searchClear"),
|
|
2179
|
+
onClick: (event) => {
|
|
2180
|
+
event.stopPropagation();
|
|
2181
|
+
store.setSearch("");
|
|
2182
|
+
setSearchExpanded(false);
|
|
2183
|
+
},
|
|
2184
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
|
|
2185
|
+
})
|
|
2186
|
+
]
|
|
2187
|
+
})
|
|
2188
|
+
}),
|
|
2189
|
+
!searchExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2190
|
+
className: SidebarRooms_module_css_default.headerActions,
|
|
2191
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
|
|
2192
|
+
t,
|
|
2193
|
+
open: viewOpen,
|
|
2194
|
+
orderBy,
|
|
2195
|
+
archivedFilter,
|
|
2196
|
+
onToggle: () => setViewOpen((v) => !v),
|
|
2197
|
+
onPickOrder: (mode) => void onPickOrder(mode),
|
|
2198
|
+
onPickArchived: (mode) => {
|
|
2199
|
+
setArchivedFilter(mode);
|
|
2200
|
+
setViewOpen(false);
|
|
2201
|
+
}
|
|
2202
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2203
|
+
label: t("createRoom"),
|
|
2204
|
+
side: "bottom",
|
|
2205
|
+
delayMs: 500,
|
|
2206
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2207
|
+
type: "button",
|
|
2208
|
+
className: SidebarRooms_module_css_default.iconButton,
|
|
2209
|
+
"aria-label": t("createRoom"),
|
|
2210
|
+
onClick: () => store.openCreate(),
|
|
2211
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconProjectAddOutlineRegular, { size: 16 })
|
|
2212
|
+
})
|
|
2213
|
+
})]
|
|
2214
|
+
})
|
|
2215
|
+
]
|
|
2216
|
+
}),
|
|
2217
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2218
|
+
className: SidebarRooms_module_css_default.listArea,
|
|
2219
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2220
|
+
className: SidebarRooms_module_css_default.list,
|
|
2221
|
+
children: [
|
|
2222
|
+
failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2223
|
+
className: SidebarRooms_module_css_default.empty,
|
|
2224
|
+
children: t(failure)
|
|
2225
|
+
}),
|
|
2226
|
+
failure === void 0 && rooms === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2227
|
+
className: SidebarRooms_module_css_default.empty,
|
|
2228
|
+
children: t("loading")
|
|
2229
|
+
}),
|
|
2230
|
+
failure === void 0 && rooms !== void 0 && visible.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2231
|
+
className: SidebarRooms_module_css_default.emptyState,
|
|
2232
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQueueOutlineRegular, { size: 28 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: query.length > 0 ? t("searchEmpty") : t("emptyRooms") })]
|
|
2233
|
+
}),
|
|
2234
|
+
visible.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2235
|
+
className: SidebarRooms_module_css_default.flatList,
|
|
2236
|
+
children: visible.map((room) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomRow, {
|
|
2237
|
+
room,
|
|
2238
|
+
t,
|
|
2239
|
+
store,
|
|
2240
|
+
selected: room.id === activeRoomId,
|
|
2241
|
+
onOpen: () => store.open(room.id)
|
|
2242
|
+
}) }, room.id))
|
|
2243
|
+
}),
|
|
2244
|
+
query.length > 0 && discoverable !== void 0 && joinable.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2245
|
+
className: SidebarRooms_module_css_default.discoverSection,
|
|
2246
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2247
|
+
className: SidebarRooms_module_css_default.discoverHeading,
|
|
2248
|
+
children: t("discoverHeading")
|
|
2249
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2250
|
+
className: SidebarRooms_module_css_default.flatList,
|
|
2251
|
+
children: joinable.map((room) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2252
|
+
className: SidebarRooms_module_css_default.discoverRow,
|
|
2253
|
+
children: [
|
|
2254
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2255
|
+
className: SidebarRooms_module_css_default.discoverName,
|
|
2256
|
+
children: room.name
|
|
2257
|
+
}),
|
|
2258
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2259
|
+
className: SidebarRooms_module_css_default.discoverMeta,
|
|
2260
|
+
children: t("discoverMembers").replace("{count}", String(room.memberCount))
|
|
2261
|
+
}),
|
|
2262
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2263
|
+
type: "button",
|
|
2264
|
+
className: SidebarRooms_module_css_default.joinButton,
|
|
2265
|
+
disabled: joining === room.id,
|
|
2266
|
+
onClick: () => {
|
|
2267
|
+
onJoin(room.id);
|
|
2268
|
+
},
|
|
2269
|
+
children: joining === room.id ? t("joining") : t("joinRoom")
|
|
2270
|
+
})
|
|
2271
|
+
]
|
|
2272
|
+
}) }, room.id))
|
|
2273
|
+
})]
|
|
2274
|
+
})
|
|
2275
|
+
]
|
|
2276
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2277
|
+
className: SidebarRooms_module_css_default.fade,
|
|
2278
|
+
"aria-hidden": "true"
|
|
2279
|
+
})]
|
|
2280
|
+
}),
|
|
2281
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreateRoomDialog, {
|
|
2282
|
+
t,
|
|
2283
|
+
tPreset,
|
|
2284
|
+
store
|
|
2285
|
+
}),
|
|
2286
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomDialogs, {
|
|
2287
|
+
t,
|
|
2288
|
+
store
|
|
2289
|
+
})
|
|
2290
|
+
]
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
/** One room row: glyph, name, and the member count where a session row shows its time. */
|
|
2294
|
+
function RoomRow(props) {
|
|
2295
|
+
const { room, t, selected, onOpen, store } = props;
|
|
2296
|
+
const [menuAnchor, setMenuAnchor] = (0, react.useState)(null);
|
|
2297
|
+
const [confirmingLeave, setConfirmingLeave] = (0, react.useState)(false);
|
|
2298
|
+
const [leaving, setLeaving] = (0, react.useState)(false);
|
|
2299
|
+
const [leaveFailure, setLeaveFailure] = (0, react.useState)(void 0);
|
|
2300
|
+
const [confirmingDissolve, setConfirmingDissolve] = (0, react.useState)(false);
|
|
2301
|
+
const [dissolving, setDissolving] = (0, react.useState)(false);
|
|
2302
|
+
const [dissolveFailure, setDissolveFailure] = (0, react.useState)(void 0);
|
|
2303
|
+
const unread = room.joined ? room.unreadCount ?? 0 : 0;
|
|
2304
|
+
const mentioned = unread > 0 && (room.mentionCount ?? 0) > 0;
|
|
2305
|
+
const closeLeave = () => {
|
|
2306
|
+
setConfirmingLeave(false);
|
|
2307
|
+
setLeaveFailure(void 0);
|
|
2308
|
+
};
|
|
2309
|
+
const closeDissolve = () => {
|
|
2310
|
+
setConfirmingDissolve(false);
|
|
2311
|
+
setDissolveFailure(void 0);
|
|
2312
|
+
};
|
|
2313
|
+
const leave = async () => {
|
|
2314
|
+
if (leaving) return;
|
|
2315
|
+
setLeaving(true);
|
|
2316
|
+
setLeaveFailure(void 0);
|
|
2317
|
+
try {
|
|
2318
|
+
await store.leave(room.id);
|
|
2319
|
+
setConfirmingLeave(false);
|
|
2320
|
+
} catch (cause) {
|
|
2321
|
+
setLeaveFailure(failureOf(cause));
|
|
2322
|
+
} finally {
|
|
2323
|
+
setLeaving(false);
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
const dissolve = async () => {
|
|
2327
|
+
if (dissolving) return;
|
|
2328
|
+
setDissolving(true);
|
|
2329
|
+
setDissolveFailure(void 0);
|
|
2330
|
+
try {
|
|
2331
|
+
await store.dissolve(room.id);
|
|
2332
|
+
setConfirmingDissolve(false);
|
|
2333
|
+
} catch (cause) {
|
|
2334
|
+
setDissolveFailure(failureOf(cause));
|
|
2335
|
+
} finally {
|
|
2336
|
+
setDissolving(false);
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2340
|
+
className: SidebarRooms_module_css_default.rowWrap,
|
|
2341
|
+
children: [
|
|
2342
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2343
|
+
type: "button",
|
|
2344
|
+
className: selected ? SidebarRooms_module_css_default.sessionRowSelected : SidebarRooms_module_css_default.sessionRow,
|
|
2345
|
+
onClick: onOpen,
|
|
2346
|
+
onContextMenu: (event) => {
|
|
2347
|
+
event.preventDefault();
|
|
2348
|
+
setMenuAnchor(event.currentTarget.getBoundingClientRect());
|
|
2349
|
+
},
|
|
2350
|
+
"aria-current": selected,
|
|
2351
|
+
children: [
|
|
2352
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2353
|
+
className: SidebarRooms_module_css_default.rowIcon,
|
|
2354
|
+
"aria-hidden": "true",
|
|
2355
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2356
|
+
width: "16",
|
|
2357
|
+
height: "16",
|
|
2358
|
+
viewBox: "0 0 16 16",
|
|
2359
|
+
fill: "none",
|
|
2360
|
+
stroke: "currentColor",
|
|
2361
|
+
strokeWidth: "1.4",
|
|
2362
|
+
strokeLinecap: "round",
|
|
2363
|
+
strokeLinejoin: "round",
|
|
2364
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2.5 3.5A1.5 1.5 0 0 1 4 2h5a1.5 1.5 0 0 1 1.5 1.5v3A1.5 1.5 0 0 1 9 8H5.5L3 10V8h-.5A1.5 1.5 0 0 1 2.5 6.5z" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M11 5.5h1A1.5 1.5 0 0 1 13.5 7v3.5A1.5 1.5 0 0 1 12 12h-.5v2l-2.5-2H7" })]
|
|
2365
|
+
})
|
|
2366
|
+
}),
|
|
2367
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2368
|
+
className: SidebarRooms_module_css_default.title,
|
|
2369
|
+
children: room.name
|
|
2370
|
+
}),
|
|
2371
|
+
unread > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2372
|
+
className: mentioned ? SidebarRooms_module_css_default.unreadMentioned : SidebarRooms_module_css_default.unread,
|
|
2373
|
+
"aria-label": mentioned ? t("unreadMentionAria").replace("{count}", String(unread)) : t("unreadAria").replace("{count}", String(unread)),
|
|
2374
|
+
children: unread > 99 ? "99+" : String(unread)
|
|
2375
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2376
|
+
className: SidebarRooms_module_css_default.time,
|
|
2377
|
+
children: room.joined ? t("memberCountShort").replace("{count}", String(room.memberCount)) : t("left")
|
|
2378
|
+
})
|
|
2379
|
+
]
|
|
2380
|
+
}),
|
|
2381
|
+
menuAnchor !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomMenu, {
|
|
2382
|
+
t,
|
|
2383
|
+
room,
|
|
2384
|
+
store,
|
|
2385
|
+
anchor: menuAnchor,
|
|
2386
|
+
onClose: () => setMenuAnchor(null),
|
|
2387
|
+
onRequestLeave: () => {
|
|
2388
|
+
setMenuAnchor(null);
|
|
2389
|
+
setConfirmingLeave(true);
|
|
2390
|
+
},
|
|
2391
|
+
onRequestDissolve: () => {
|
|
2392
|
+
setMenuAnchor(null);
|
|
2393
|
+
setConfirmingDissolve(true);
|
|
2394
|
+
}
|
|
2395
|
+
}),
|
|
2396
|
+
confirmingLeave && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LeaveRoomDialog, {
|
|
2397
|
+
t,
|
|
2398
|
+
room,
|
|
2399
|
+
busy: leaving,
|
|
2400
|
+
failure: leaveFailure,
|
|
2401
|
+
onCancel: closeLeave,
|
|
2402
|
+
onConfirm: () => void leave()
|
|
2403
|
+
}),
|
|
2404
|
+
confirmingDissolve && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DissolveRoomDialog, {
|
|
2405
|
+
t,
|
|
2406
|
+
room,
|
|
2407
|
+
busy: dissolving,
|
|
2408
|
+
failure: dissolveFailure,
|
|
2409
|
+
onCancel: closeDissolve,
|
|
2410
|
+
onConfirm: () => void dissolve()
|
|
2411
|
+
})
|
|
2412
|
+
]
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* The row's context menu.
|
|
2417
|
+
*
|
|
2418
|
+
* Two official workspace entries are absent here rather than relabelled. Archiving would
|
|
2419
|
+
* duplicate "leave room", and forking copies history whose events carry no member
|
|
2420
|
+
* attribution — producing a room that looks continuous but whose earlier messages belong
|
|
2421
|
+
* to nobody. The remaining entries are the ones the design settled on, and each is shown
|
|
2422
|
+
* only when it applies to this room and this caller.
|
|
2423
|
+
*/
|
|
2424
|
+
function RoomMenu(props) {
|
|
2425
|
+
const { t, room, store, onClose, anchor, onRequestLeave, onRequestDissolve } = props;
|
|
2426
|
+
const [renaming, setRenaming] = (0, react.useState)(false);
|
|
2427
|
+
const [draftName, setDraftName] = (0, react.useState)(room.name);
|
|
2428
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2429
|
+
const run = async (action) => {
|
|
2430
|
+
setBusy(true);
|
|
2431
|
+
try {
|
|
2432
|
+
await action();
|
|
2433
|
+
onClose();
|
|
2434
|
+
} finally {
|
|
2435
|
+
setBusy(false);
|
|
2436
|
+
}
|
|
2437
|
+
};
|
|
2438
|
+
const below = window.innerHeight - anchor.bottom - 8;
|
|
2439
|
+
const placement = below < 220 && anchor.top > below ? { bottom: window.innerHeight - anchor.top + 4 } : { top: anchor.bottom + 4 };
|
|
2440
|
+
const portal = (body) => (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2441
|
+
className: SidebarRooms_module_css_default.menuLayer,
|
|
2442
|
+
style: {
|
|
2443
|
+
...placement,
|
|
2444
|
+
left: anchor.left
|
|
2445
|
+
},
|
|
2446
|
+
onClick: (event) => event.stopPropagation(),
|
|
2447
|
+
children: body
|
|
2448
|
+
}), document.body);
|
|
2449
|
+
if (renaming) {
|
|
2450
|
+
const commit = () => {
|
|
2451
|
+
run(() => store.rename(room.id, draftName.trim()));
|
|
2452
|
+
};
|
|
2453
|
+
const cancel = () => {
|
|
2454
|
+
setRenaming(false);
|
|
2455
|
+
setDraftName(room.name);
|
|
2456
|
+
};
|
|
2457
|
+
return portal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2458
|
+
className: SidebarRooms_module_css_default.menuRename,
|
|
2459
|
+
role: "dialog",
|
|
2460
|
+
"aria-label": t("renameTitle"),
|
|
2461
|
+
children: [
|
|
2462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2463
|
+
className: SidebarRooms_module_css_default.menuHeading,
|
|
2464
|
+
children: t("renameTitle")
|
|
2465
|
+
}),
|
|
2466
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2467
|
+
className: SidebarRooms_module_css_default.menuInput,
|
|
2468
|
+
value: draftName,
|
|
2469
|
+
autoFocus: true,
|
|
2470
|
+
placeholder: t("renamePlaceholder"),
|
|
2471
|
+
onChange: (event) => setDraftName(event.target.value),
|
|
2472
|
+
onKeyDown: (event) => {
|
|
2473
|
+
if (event.key === "Enter" && draftName.trim().length > 0) commit();
|
|
2474
|
+
if (event.key === "Escape") cancel();
|
|
2475
|
+
}
|
|
2476
|
+
}),
|
|
2477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2478
|
+
className: SidebarRooms_module_css_default.menuActions,
|
|
2479
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2480
|
+
type: "button",
|
|
2481
|
+
className: SidebarRooms_module_css_default.menuCancel,
|
|
2482
|
+
onClick: cancel,
|
|
2483
|
+
children: t("cancel")
|
|
2484
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2485
|
+
type: "button",
|
|
2486
|
+
className: SidebarRooms_module_css_default.menuConfirm,
|
|
2487
|
+
disabled: busy || draftName.trim().length === 0,
|
|
2488
|
+
onClick: commit,
|
|
2489
|
+
children: t("save")
|
|
2490
|
+
})]
|
|
2491
|
+
})
|
|
2492
|
+
]
|
|
2493
|
+
}));
|
|
2494
|
+
}
|
|
2495
|
+
return portal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2496
|
+
className: SidebarRooms_module_css_default.menu,
|
|
2497
|
+
role: "menu",
|
|
2498
|
+
children: [
|
|
2499
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2500
|
+
type: "button",
|
|
2501
|
+
role: "menuitem",
|
|
2502
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2503
|
+
onClick: () => setRenaming(true),
|
|
2504
|
+
children: t("menuRename")
|
|
2505
|
+
}),
|
|
2506
|
+
room.joined && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2507
|
+
type: "button",
|
|
2508
|
+
role: "menuitem",
|
|
2509
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2510
|
+
onClick: () => {
|
|
2511
|
+
store.setDialog({
|
|
2512
|
+
kind: "invite",
|
|
2513
|
+
roomId: room.id
|
|
2514
|
+
});
|
|
2515
|
+
onClose();
|
|
2516
|
+
},
|
|
2517
|
+
children: t("menuInvite")
|
|
2518
|
+
}),
|
|
2519
|
+
store.openMembers !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2520
|
+
type: "button",
|
|
2521
|
+
role: "menuitem",
|
|
2522
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2523
|
+
onClick: () => {
|
|
2524
|
+
store.openMembers?.(room.id);
|
|
2525
|
+
onClose();
|
|
2526
|
+
},
|
|
2527
|
+
children: t("menuMembers")
|
|
2528
|
+
}),
|
|
2529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2530
|
+
type: "button",
|
|
2531
|
+
role: "menuitem",
|
|
2532
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2533
|
+
onClick: () => {
|
|
2534
|
+
store.setDialog({
|
|
2535
|
+
kind: "settings",
|
|
2536
|
+
roomId: room.id
|
|
2537
|
+
});
|
|
2538
|
+
onClose();
|
|
2539
|
+
},
|
|
2540
|
+
children: t("menuSettings")
|
|
2541
|
+
}),
|
|
2542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2543
|
+
type: "button",
|
|
2544
|
+
role: "menuitem",
|
|
2545
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2546
|
+
onClick: () => {
|
|
2547
|
+
navigator.clipboard?.writeText(room.id);
|
|
2548
|
+
onClose();
|
|
2549
|
+
},
|
|
2550
|
+
children: t("copyRoomId")
|
|
2551
|
+
}),
|
|
2552
|
+
room.joined && store.selfId() === room.ownerUserId && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2553
|
+
type: "button",
|
|
2554
|
+
role: "menuitem",
|
|
2555
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2556
|
+
onClick: () => {
|
|
2557
|
+
store.setDialog({
|
|
2558
|
+
kind: "transfer",
|
|
2559
|
+
roomId: room.id
|
|
2560
|
+
});
|
|
2561
|
+
onClose();
|
|
2562
|
+
},
|
|
2563
|
+
children: t("menuTransfer")
|
|
2564
|
+
}),
|
|
2565
|
+
room.joined && store.selfId() !== room.ownerUserId && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2566
|
+
type: "button",
|
|
2567
|
+
role: "menuitem",
|
|
2568
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2569
|
+
onClick: onRequestLeave,
|
|
2570
|
+
children: t("menuLeave")
|
|
2571
|
+
}),
|
|
2572
|
+
(store.selfId() === room.ownerUserId || store.administrator()) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2573
|
+
type: "button",
|
|
2574
|
+
role: "menuitem",
|
|
2575
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2576
|
+
onClick: onRequestDissolve,
|
|
2577
|
+
children: t("menuDissolve")
|
|
2578
|
+
})
|
|
2579
|
+
]
|
|
2580
|
+
}));
|
|
2581
|
+
}
|
|
2582
|
+
/**
|
|
2583
|
+
* The dissolve-room confirmation.
|
|
2584
|
+
*
|
|
2585
|
+
* A separate dialog from leaving, not a variant of it: leaving removes one membership and the
|
|
2586
|
+
* room carries on for everyone else, while dissolving ends it for every member and takes the
|
|
2587
|
+
* messages with it. Sharing one dialog would let the far more destructive act read as the
|
|
2588
|
+
* mild one.
|
|
2589
|
+
*
|
|
2590
|
+
* Like the leave dialog, a refusal keeps this open and prints the mapped reason: the room is
|
|
2591
|
+
* still there, so closing would read as success.
|
|
2592
|
+
*/
|
|
2593
|
+
function DissolveRoomDialog(props) {
|
|
2594
|
+
const { t, room, busy, failure, onCancel, onConfirm } = props;
|
|
2595
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2596
|
+
open: true,
|
|
2597
|
+
onClose: onCancel,
|
|
2598
|
+
title: t("menuDissolve"),
|
|
2599
|
+
closeLabel: t("cancel"),
|
|
2600
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2601
|
+
className: SidebarRooms_module_css_default.footer,
|
|
2602
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2603
|
+
"data-modal-autofocus": true,
|
|
2604
|
+
variant: "ghost",
|
|
2605
|
+
size: "md",
|
|
2606
|
+
disabled: busy,
|
|
2607
|
+
onClick: onCancel,
|
|
2608
|
+
children: t("cancel")
|
|
2609
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2610
|
+
variant: "ghost",
|
|
2611
|
+
size: "md",
|
|
2612
|
+
disabled: busy,
|
|
2613
|
+
className: SidebarRooms_module_css_default.leaveConfirmDanger,
|
|
2614
|
+
onClick: onConfirm,
|
|
2615
|
+
children: t("menuDissolve")
|
|
2616
|
+
})]
|
|
2617
|
+
}),
|
|
2618
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2619
|
+
className: SidebarRooms_module_css_default.leaveBody,
|
|
2620
|
+
children: [
|
|
2621
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2622
|
+
className: SidebarRooms_module_css_default.leaveCopy,
|
|
2623
|
+
children: t("menuDissolveConfirm")
|
|
2624
|
+
}),
|
|
2625
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2626
|
+
className: SidebarRooms_module_css_default.leaveRoomName,
|
|
2627
|
+
children: [t("menuDissolveTarget"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: room.name })]
|
|
2628
|
+
}),
|
|
2629
|
+
failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2630
|
+
className: SidebarRooms_module_css_default.leaveError,
|
|
2631
|
+
role: "alert",
|
|
2632
|
+
children: t(failure)
|
|
2633
|
+
})
|
|
2634
|
+
]
|
|
2635
|
+
})
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* The leave-room confirmation.
|
|
2640
|
+
*
|
|
2641
|
+
* It exists because leaving is not undoable from this surface: the sidebar keeps the room
|
|
2642
|
+
* under the "left rooms" filter, but rejoining is a separate discovery step. The copy names
|
|
2643
|
+
* that the agent's recorded history stays, which is the part a reader cannot infer.
|
|
2644
|
+
*
|
|
2645
|
+
* A refused leave keeps this dialog open and prints the mapped failure. Closing on failure
|
|
2646
|
+
* was the old behaviour and read as success — the only sign it had gone wrong was the room
|
|
2647
|
+
* still being in the list.
|
|
2648
|
+
*/
|
|
2649
|
+
function LeaveRoomDialog(props) {
|
|
2650
|
+
const { t, room, busy, failure, onCancel, onConfirm } = props;
|
|
2651
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2652
|
+
open: true,
|
|
2653
|
+
onClose: onCancel,
|
|
2654
|
+
title: t("menuLeave"),
|
|
2655
|
+
closeLabel: t("cancel"),
|
|
2656
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2657
|
+
className: SidebarRooms_module_css_default.footer,
|
|
2658
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2659
|
+
"data-modal-autofocus": true,
|
|
2660
|
+
variant: "ghost",
|
|
2661
|
+
size: "md",
|
|
2662
|
+
disabled: busy,
|
|
2663
|
+
onClick: onCancel,
|
|
2664
|
+
children: t("cancel")
|
|
2665
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2666
|
+
variant: "primary",
|
|
2667
|
+
size: "md",
|
|
2668
|
+
disabled: busy,
|
|
2669
|
+
onClick: onConfirm,
|
|
2670
|
+
children: t("menuLeave")
|
|
2671
|
+
})]
|
|
2672
|
+
}),
|
|
2673
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2674
|
+
className: SidebarRooms_module_css_default.leaveBody,
|
|
2675
|
+
children: [
|
|
2676
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2677
|
+
className: SidebarRooms_module_css_default.leaveCopy,
|
|
2678
|
+
children: t("menuLeaveConfirm")
|
|
2679
|
+
}),
|
|
2680
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2681
|
+
className: SidebarRooms_module_css_default.leaveRoomName,
|
|
2682
|
+
children: room.name
|
|
2683
|
+
}),
|
|
2684
|
+
failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2685
|
+
className: SidebarRooms_module_css_default.leaveError,
|
|
2686
|
+
role: "alert",
|
|
2687
|
+
children: t(failure)
|
|
2688
|
+
})
|
|
2689
|
+
]
|
|
2690
|
+
})
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
/** The trailing view menu: ordering and whether rooms the operator left are listed. */
|
|
2694
|
+
function ViewOptionsMenu(props) {
|
|
2695
|
+
const { t, open, orderBy, archivedFilter, onToggle, onPickOrder, onPickArchived } = props;
|
|
2696
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2697
|
+
className: SidebarRooms_module_css_default.viewWrap,
|
|
2698
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2699
|
+
label: t("viewOptions"),
|
|
2700
|
+
side: "bottom",
|
|
2701
|
+
delayMs: 500,
|
|
2702
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2703
|
+
type: "button",
|
|
2704
|
+
className: SidebarRooms_module_css_default.iconButton,
|
|
2705
|
+
"aria-label": t("viewOptions"),
|
|
2706
|
+
"aria-expanded": open,
|
|
2707
|
+
onClick: onToggle,
|
|
2708
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, { size: 16 })
|
|
2709
|
+
})
|
|
2710
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2711
|
+
className: SidebarRooms_module_css_default.menuInline,
|
|
2712
|
+
role: "menu",
|
|
2713
|
+
children: [
|
|
2714
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2715
|
+
className: SidebarRooms_module_css_default.menuHeading,
|
|
2716
|
+
children: t("orderBy")
|
|
2717
|
+
}),
|
|
2718
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2719
|
+
type: "button",
|
|
2720
|
+
role: "menuitem",
|
|
2721
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2722
|
+
onClick: () => onPickOrder("updated"),
|
|
2723
|
+
children: [
|
|
2724
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, { size: 14 }),
|
|
2725
|
+
t("orderByUpdated"),
|
|
2726
|
+
orderBy === "updated" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2727
|
+
className: SidebarRooms_module_css_default.check,
|
|
2728
|
+
children: "✓"
|
|
2729
|
+
})
|
|
2730
|
+
]
|
|
2731
|
+
}),
|
|
2732
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2733
|
+
type: "button",
|
|
2734
|
+
role: "menuitem",
|
|
2735
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2736
|
+
onClick: () => onPickOrder("manual"),
|
|
2737
|
+
children: [
|
|
2738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, { size: 14 }),
|
|
2739
|
+
t("orderByManual"),
|
|
2740
|
+
orderBy === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2741
|
+
className: SidebarRooms_module_css_default.check,
|
|
2742
|
+
children: "✓"
|
|
2743
|
+
})
|
|
2744
|
+
]
|
|
2745
|
+
}),
|
|
2746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2747
|
+
className: SidebarRooms_module_css_default.menuHeading,
|
|
2748
|
+
children: t("archivedFilter")
|
|
2749
|
+
}),
|
|
2750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2751
|
+
type: "button",
|
|
2752
|
+
role: "menuitem",
|
|
2753
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2754
|
+
onClick: () => onPickArchived("default"),
|
|
2755
|
+
children: [t("archivedHide"), archivedFilter === "default" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2756
|
+
className: SidebarRooms_module_css_default.check,
|
|
2757
|
+
children: "✓"
|
|
2758
|
+
})]
|
|
2759
|
+
}),
|
|
2760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2761
|
+
type: "button",
|
|
2762
|
+
role: "menuitem",
|
|
2763
|
+
className: SidebarRooms_module_css_default.menuItem,
|
|
2764
|
+
onClick: () => onPickArchived("all"),
|
|
2765
|
+
children: [t("archivedShow"), archivedFilter === "all" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2766
|
+
className: SidebarRooms_module_css_default.check,
|
|
2767
|
+
children: "✓"
|
|
2768
|
+
})]
|
|
2769
|
+
})
|
|
2770
|
+
]
|
|
2771
|
+
})]
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
//#endregion
|
|
2775
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/MessageSide.module.css.mjs
|
|
2776
|
+
const css$2 = ".KXabfq_otherRow{display:contents}.KXabfq_otherRow.KXabfq_otherRow>*{align-items:flex-start}.KXabfq_otherRow{flex-direction:column;align-items:flex-start;min-width:0;max-width:100%;display:flex}.KXabfq_author{min-width:0;max-width:100%;color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;margin:0 0 4px 2px;font-size:12px;line-height:16px;display:flex}.KXabfq_authorName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.KXabfq_otherRow.KXabfq_otherRow>:not(.KXabfq_author){align-items:flex-start;max-width:100%}";
|
|
2777
|
+
const tagId$2 = "@sparkelf/dsh-group-chat/MessageSide.module.css";
|
|
2778
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
2779
|
+
const tag = document.createElement("style");
|
|
2780
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
2781
|
+
tag.dataset.pluginCss = tagId$2;
|
|
2782
|
+
tag.textContent = css$2;
|
|
2783
|
+
document.head.appendChild(tag);
|
|
2784
|
+
}
|
|
2785
|
+
var MessageSide_module_css_default = {
|
|
2786
|
+
"authorName": "KXabfq_authorName",
|
|
2787
|
+
"author": "KXabfq_author",
|
|
2788
|
+
"otherRow": "KXabfq_otherRow"
|
|
2789
|
+
};
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region src/client/MessageSide.tsx
|
|
2792
|
+
/**
|
|
2793
|
+
* Left/right placement for group-chat messages.
|
|
2794
|
+
*
|
|
2795
|
+
* The official Chat renderer draws every user message right-aligned — its own CSS is
|
|
2796
|
+
* unconditional (`align-items: flex-end`), with no self/other distinction, because in an
|
|
2797
|
+
* ordinary Session the only author is the operator. A room has several authors, so the
|
|
2798
|
+
* side has to carry a meaning the official renderer does not compute: this workspace's own
|
|
2799
|
+
* messages stay right, everyone else's move left.
|
|
2800
|
+
*
|
|
2801
|
+
* It is implemented by **wrapping the official renderer, not replacing it**. The official
|
|
2802
|
+
* component is read back out of the slot registry and rendered as an element, so markdown,
|
|
2803
|
+
* images, file and skill references, per-message actions and the streaming behaviour all
|
|
2804
|
+
* stay the official implementation's. Re-drawing the bubble here would have to track every
|
|
2805
|
+
* one of those forever.
|
|
2806
|
+
*
|
|
2807
|
+
* An ordinary Session is untouched: a message with no room source is rendered through the
|
|
2808
|
+
* official component with no wrapper at all, which is what keeps the two paths equal in
|
|
2809
|
+
* what they draw.
|
|
2810
|
+
*
|
|
2811
|
+
* Another author's message also carries their name and picture above the bubble. The official
|
|
2812
|
+
* renderer draws neither — in an ordinary Session every message is the operator's own, so there
|
|
2813
|
+
* is no author to attribute — and the event itself carries only the author's id. The name and
|
|
2814
|
+
* picture therefore come from the roster, looked up by that id. When the roster has not loaded,
|
|
2815
|
+
* or the author has since left, the message renders bare rather than labelled with an id: a
|
|
2816
|
+
* wrong name is worse than no name.
|
|
2817
|
+
*
|
|
2818
|
+
* @module @sparkelf/dsh-group-chat/client/MessageSide
|
|
2819
|
+
*/
|
|
2820
|
+
/**
|
|
2821
|
+
* Which side a message belongs on, or `undefined` when it is not a room message.
|
|
2822
|
+
*
|
|
2823
|
+
* `roomId` is the discriminator: the Host stamps it on every message it admits to a room,
|
|
2824
|
+
* and an ordinary Session's message carries `{kind, rpcId, clientTimeZone}` instead. The
|
|
2825
|
+
* remaining cases split the room's own authors — an agent is always someone else, and a
|
|
2826
|
+
* human is "self" only when the id matches this workspace's account. A message whose
|
|
2827
|
+
* author cannot be identified is placed left, because assuming "self" would silently
|
|
2828
|
+
* attribute another member's words to the operator.
|
|
2829
|
+
*
|
|
2830
|
+
* @param message - the message data the node carries.
|
|
2831
|
+
* @param selfId - this workspace's own account id, when known.
|
|
2832
|
+
* @returns `'self'`, `'other'`, or `undefined` for a non-room message.
|
|
2833
|
+
*/
|
|
2834
|
+
const messageSide = (message, selfId) => {
|
|
2835
|
+
const source = message?.source;
|
|
2836
|
+
if (source === void 0 || source === null) return void 0;
|
|
2837
|
+
if (typeof source.roomId !== "string" || source.roomId.length === 0) return void 0;
|
|
2838
|
+
if (source.authorKind === "agent") return "other";
|
|
2839
|
+
return selfId !== void 0 && source.memberId === selfId ? "self" : "other";
|
|
2840
|
+
};
|
|
2841
|
+
/**
|
|
2842
|
+
* The roster entry for one message's author, when the room knows it.
|
|
2843
|
+
*
|
|
2844
|
+
* The bubble needs the name and the picture, and neither is on the event: a message carries only
|
|
2845
|
+
* `source.memberId`. The roster is the only place those two facts live, so this is the lookup
|
|
2846
|
+
* that lets another author's message say who wrote it.
|
|
2847
|
+
*
|
|
2848
|
+
* @param message - the message data the node carries.
|
|
2849
|
+
* @param roster - that room's members, or `undefined` before the roster has been read.
|
|
2850
|
+
* @returns the matching member, or `undefined` when the author is unknown or has left.
|
|
2851
|
+
*/
|
|
2852
|
+
const authorOf = (message, roster) => {
|
|
2853
|
+
const memberId = message?.source?.memberId;
|
|
2854
|
+
if (typeof memberId !== "string" || memberId.length === 0) return void 0;
|
|
2855
|
+
return roster?.find((member) => member.userId === memberId);
|
|
2856
|
+
};
|
|
2857
|
+
/**
|
|
2858
|
+
* The name to draw above another author's message.
|
|
2859
|
+
*
|
|
2860
|
+
* Falls back to the roster's display name only, never to the id: an unlabelled bubble is
|
|
2861
|
+
* readable, but a bubble labelled `u_7f3a91` is not, and it would also leak an internal id into
|
|
2862
|
+
* the conversation.
|
|
2863
|
+
*
|
|
2864
|
+
* @param member - the author's roster entry.
|
|
2865
|
+
* @returns the name, or `undefined` when the roster has no usable one.
|
|
2866
|
+
*/
|
|
2867
|
+
const authorNameOf = (member) => {
|
|
2868
|
+
const name = member?.displayName?.trim() ?? "";
|
|
2869
|
+
return name.length === 0 ? void 0 : name;
|
|
2870
|
+
};
|
|
2871
|
+
/**
|
|
2872
|
+
* Render one node with its room side applied.
|
|
2873
|
+
*
|
|
2874
|
+
* Two details this has to get right, both of which fail silently otherwise:
|
|
2875
|
+
*
|
|
2876
|
+
* The official view is created as a React **element**, never called as a plain function. It
|
|
2877
|
+
* is a `React.memo` component, which compiles to a non-callable object, so calling it throws
|
|
2878
|
+
* `TypeError: … is not a function`; and even when a view is callable, calling it directly
|
|
2879
|
+
* would run its Hooks as this component's and drop its memoization.
|
|
2880
|
+
*
|
|
2881
|
+
* The account id is read through `useSyncExternalStore` rather than captured. It is fetched
|
|
2882
|
+
* after the first paint, and a value captured when this renderer was registered would stay
|
|
2883
|
+
* `undefined` for the life of the page — every message would keep the unknown-author
|
|
2884
|
+
* placement, and nothing would ever correct it. The store is the same observable the room
|
|
2885
|
+
* list uses, and the snapshot is a plain string, so an unrelated room-list change notifies
|
|
2886
|
+
* without re-rendering: React compares the snapshot by identity.
|
|
2887
|
+
*
|
|
2888
|
+
* @param props - the official props plus this wrapper's placement inputs.
|
|
2889
|
+
* @returns the official view, wrapped only when the message is another author's room message.
|
|
2890
|
+
*/
|
|
2891
|
+
const MessageSide = (props) => {
|
|
2892
|
+
const { node, Official, getSelfId, subscribeSelf, sided, getRoster } = props;
|
|
2893
|
+
const selfId = (0, react.useSyncExternalStore)(subscribeSelf, getSelfId, getSelfId);
|
|
2894
|
+
if (typeof Official !== "function" && typeof Official !== "object") return null;
|
|
2895
|
+
const view = (0, react.createElement)(Official, props);
|
|
2896
|
+
const message = node?.data;
|
|
2897
|
+
if ((sided ? messageSide(message, selfId) : void 0) !== "other") return view;
|
|
2898
|
+
const roomId = message?.source?.roomId;
|
|
2899
|
+
const member = authorOf(message, typeof roomId === "string" && roomId.length > 0 ? getRoster(roomId) : void 0);
|
|
2900
|
+
const name = authorNameOf(member);
|
|
2901
|
+
const head = name === void 0 || member === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2902
|
+
className: MessageSide_module_css_default["author"],
|
|
2903
|
+
"data-group-chat-author": member.userId,
|
|
2904
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
2905
|
+
name,
|
|
2906
|
+
src: member.avatarUrl,
|
|
2907
|
+
size: "sm"
|
|
2908
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2909
|
+
className: MessageSide_module_css_default["authorName"],
|
|
2910
|
+
children: name
|
|
2911
|
+
})]
|
|
2912
|
+
});
|
|
2913
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2914
|
+
className: MessageSide_module_css_default["otherRow"],
|
|
2915
|
+
"data-group-chat-side": "other",
|
|
2916
|
+
children: [head, view]
|
|
2917
|
+
});
|
|
2918
|
+
};
|
|
2919
|
+
//#endregion
|
|
2920
|
+
//#region src/session-id.ts
|
|
2921
|
+
/**
|
|
2922
|
+
* The room/session binding, shared by both halves of the plugin.
|
|
2923
|
+
*
|
|
2924
|
+
* The browser half needs it to open a room's Session; the host half needs it to create and
|
|
2925
|
+
* recognize one. It lives in its own module because the two halves are built for different
|
|
2926
|
+
* runtimes: importing it from the host entry would drag the whole server side into the client
|
|
2927
|
+
* bundle, which the build's platform guard refuses — correctly, since none of it can run there.
|
|
2928
|
+
*
|
|
2929
|
+
* @module @sparkelf/dsh-group-chat/session-id
|
|
2930
|
+
*/
|
|
2931
|
+
/**
|
|
2932
|
+
* Every room session id starts with this, so a room's session is recognizable after a restart
|
|
2933
|
+
* and an ordinary session never matches.
|
|
2934
|
+
*
|
|
2935
|
+
* The binding lives in the id rather than in `SessionHeader` because the header carries no
|
|
2936
|
+
* free-form metadata field.
|
|
2937
|
+
*/
|
|
2938
|
+
const ROOM_SESSION_PREFIX = "group-room-";
|
|
2939
|
+
/** Deterministic session id for a room. */
|
|
2940
|
+
const roomSessionId = (roomId) => `${ROOM_SESSION_PREFIX}${roomId}`;
|
|
2941
|
+
/** Reads the room a session is bound to, or `undefined` for an ordinary session. */
|
|
2942
|
+
const roomIdOfSession = (session) => {
|
|
2943
|
+
if (typeof session !== "object" || session === null) return void 0;
|
|
2944
|
+
const id = session.id;
|
|
2945
|
+
if (typeof id !== "string" || !id.startsWith("group-room-")) return void 0;
|
|
2946
|
+
const roomId = id.slice(11);
|
|
2947
|
+
return roomId.length > 0 ? roomId : void 0;
|
|
2948
|
+
};
|
|
2949
|
+
//#endregion
|
|
2950
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/MembersPanel.module.css.mjs
|
|
2951
|
+
const css$1 = ".VeQVAa_root{box-sizing:border-box;flex-direction:column;gap:16px;min-width:200px;padding:12px 12px 20px;display:flex}.VeQVAa_section{flex-direction:column;gap:8px;min-width:0;display:flex}.VeQVAa_section+.VeQVAa_section{border-top:1px solid var(--dsw-alias-border-l3);padding-top:14px}.VeQVAa_sectionHeader{align-items:center;gap:6px;min-width:0;display:flex}.VeQVAa_sectionToggle{min-width:0;color:inherit;font:inherit;cursor:pointer;text-align:start;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:flex}.VeQVAa_caret{width:10px;color:var(--dsw-alias-label-tertiary);flex:none;font-size:10px;line-height:12px}.VeQVAa_sectionBody{flex-direction:column;gap:8px;min-width:0;display:flex}.VeQVAa_sectionLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px}.VeQVAa_count{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.VeQVAa_searchToggle{border-radius:var(--dsw-radius-sm);width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;flex:none;justify-content:center;align-items:center;margin-left:auto;padding:0;display:flex}.VeQVAa_searchToggle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.VeQVAa_searchToggle:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.VeQVAa_linkButton{border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-business-primary);font:inherit;cursor:pointer;background:0 0;border:none;margin-left:auto;padding:2px 6px;font-size:12px}.VeQVAa_linkButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.VeQVAa_announcementText{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;margin:0;padding:8px 10px;font-size:13px;line-height:20px}.VeQVAa_editor{flex-direction:column;gap:8px;display:flex}.VeQVAa_textarea{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-sm);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;background:0 0;padding:8px 10px;font-size:13px;line-height:20px}.VeQVAa_textarea:focus{border-color:var(--dsw-alias-state-business-primary);outline:none}.VeQVAa_editorFooter{align-items:center;gap:6px;display:flex}.VeQVAa_editorFooter .VeQVAa_hint{margin-right:auto}.VeQVAa_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:16px}.VeQVAa_unavailable{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}.VeQVAa_error{color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-primary));margin:0;font-size:12px;line-height:16px}.VeQVAa_notice{color:var(--dsw-alias-label-tertiary);margin:0;padding:16px 12px;font-size:12px;line-height:18px}.VeQVAa_search{min-width:0}.VeQVAa_list{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.VeQVAa_row{border-radius:var(--dsw-radius-sm);align-items:center;gap:8px;min-width:0;padding:5px 4px;display:flex}.VeQVAa_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.VeQVAa_name{min-width:0;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:auto;font-size:13px;line-height:18px;overflow:hidden}.VeQVAa_fileButton{width:100%;color:inherit;font:inherit;text-align:start;cursor:pointer;border-radius:var(--dsw-alias-border-radius-s);background:0 0;border:0;align-items:center;gap:8px;padding:0;display:flex}.VeQVAa_fileButton:hover .VeQVAa_name{text-decoration:underline}.VeQVAa_filePicker{display:none}.VeQVAa_crumbs{flex-wrap:wrap;align-items:center;gap:2px;min-width:0;display:flex}.VeQVAa_crumbGroup{align-items:center;gap:2px;min-width:0;display:inline-flex}.VeQVAa_crumb{text-overflow:ellipsis;white-space:nowrap;max-width:120px;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;padding:0 2px;font-size:11px;overflow:hidden}.VeQVAa_crumb:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.VeQVAa_crumbSep{color:var(--dsw-alias-label-tertiary);font-size:11px}.VeQVAa_refresh{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;margin-inline-start:auto;padding:0 4px;font-size:12px;line-height:16px}.VeQVAa_refresh:hover{color:var(--dsw-alias-label-primary)}.VeQVAa_preview{border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-alias-border-radius-m);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-base));margin-top:8px;overflow:hidden}.VeQVAa_previewHeader{border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:6px 8px;display:flex}.VeQVAa_previewTitle{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;font-size:12px;font-weight:500;overflow:hidden}.VeQVAa_previewClose{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;flex:none;padding:0 4px;font-size:14px;line-height:16px}.VeQVAa_previewClose:hover{color:var(--dsw-alias-label-primary)}.VeQVAa_previewImage{border-radius:var(--dsw-alias-border-radius-s);background:var(--dsw-alias-bg-base);max-width:100%;height:auto;display:block}.VeQVAa_previewBody{max-height:240px;font-family:var(--dsw-alias-font-family-mono,monospace);white-space:pre-wrap;word-break:break-word;margin:0;padding:8px;font-size:11px;line-height:16px;overflow:auto}.VeQVAa_fileIcon{text-align:center;flex:none;width:16px;font-size:12px;line-height:16px}.VeQVAa_fileSize{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.VeQVAa_remove{border-radius:var(--dsw-radius-sm);width:20px;height:20px;color:var(--dsw-alias-label-tertiary);opacity:0;cursor:pointer;background:0 0;border:none;flex:none;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background .12s,color .12s;display:inline-flex}.VeQVAa_row:hover .VeQVAa_remove,.VeQVAa_remove:focus-visible{opacity:1}.VeQVAa_remove:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-state-error-primary)}.VeQVAa_selfTag{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);flex:none;padding:0 5px;font-size:11px;line-height:16px}.VeQVAa_ownerTag{border:1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary,#f7ad31) 40%, transparent);border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-state-warning-primary,#f7ad31);flex:none;padding:0 5px;font-size:11px;line-height:16px}";
|
|
2952
|
+
const tagId$1 = "@sparkelf/dsh-group-chat/MembersPanel.module.css";
|
|
2953
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
2954
|
+
const tag = document.createElement("style");
|
|
2955
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
2956
|
+
tag.dataset.pluginCss = tagId$1;
|
|
2957
|
+
tag.textContent = css$1;
|
|
2958
|
+
document.head.appendChild(tag);
|
|
2959
|
+
}
|
|
2960
|
+
var MembersPanel_module_css_default = {
|
|
2961
|
+
"announcementText": "VeQVAa_announcementText",
|
|
2962
|
+
"notice": "VeQVAa_notice",
|
|
2963
|
+
"name": "VeQVAa_name",
|
|
2964
|
+
"previewClose": "VeQVAa_previewClose",
|
|
2965
|
+
"unavailable": "VeQVAa_unavailable",
|
|
2966
|
+
"crumbGroup": "VeQVAa_crumbGroup",
|
|
2967
|
+
"textarea": "VeQVAa_textarea",
|
|
2968
|
+
"search": "VeQVAa_search",
|
|
2969
|
+
"linkButton": "VeQVAa_linkButton",
|
|
2970
|
+
"fileButton": "VeQVAa_fileButton",
|
|
2971
|
+
"previewImage": "VeQVAa_previewImage",
|
|
2972
|
+
"section": "VeQVAa_section",
|
|
2973
|
+
"sectionToggle": "VeQVAa_sectionToggle",
|
|
2974
|
+
"fileSize": "VeQVAa_fileSize",
|
|
2975
|
+
"crumbSep": "VeQVAa_crumbSep",
|
|
2976
|
+
"selfTag": "VeQVAa_selfTag",
|
|
2977
|
+
"ownerTag": "VeQVAa_ownerTag",
|
|
2978
|
+
"sectionBody": "VeQVAa_sectionBody",
|
|
2979
|
+
"preview": "VeQVAa_preview",
|
|
2980
|
+
"previewTitle": "VeQVAa_previewTitle",
|
|
2981
|
+
"sectionHeader": "VeQVAa_sectionHeader",
|
|
2982
|
+
"remove": "VeQVAa_remove",
|
|
2983
|
+
"editor": "VeQVAa_editor",
|
|
2984
|
+
"error": "VeQVAa_error",
|
|
2985
|
+
"caret": "VeQVAa_caret",
|
|
2986
|
+
"count": "VeQVAa_count",
|
|
2987
|
+
"crumb": "VeQVAa_crumb",
|
|
2988
|
+
"row": "VeQVAa_row",
|
|
2989
|
+
"filePicker": "VeQVAa_filePicker",
|
|
2990
|
+
"refresh": "VeQVAa_refresh",
|
|
2991
|
+
"previewHeader": "VeQVAa_previewHeader",
|
|
2992
|
+
"editorFooter": "VeQVAa_editorFooter",
|
|
2993
|
+
"root": "VeQVAa_root",
|
|
2994
|
+
"previewBody": "VeQVAa_previewBody",
|
|
2995
|
+
"fileIcon": "VeQVAa_fileIcon",
|
|
2996
|
+
"sectionLabel": "VeQVAa_sectionLabel",
|
|
2997
|
+
"hint": "VeQVAa_hint",
|
|
2998
|
+
"searchToggle": "VeQVAa_searchToggle",
|
|
2999
|
+
"list": "VeQVAa_list",
|
|
3000
|
+
"crumbs": "VeQVAa_crumbs"
|
|
3001
|
+
};
|
|
3002
|
+
//#endregion
|
|
3003
|
+
//#region src/client/MembersPanel.tsx
|
|
3004
|
+
/**
|
|
3005
|
+
* The members panel, shown in the right sidebar beside a room's conversation.
|
|
3006
|
+
*
|
|
3007
|
+
* A room is a Session, so the shell already draws its messages; what the shell has no reason to
|
|
3008
|
+
* know about is the roster behind them. This panel is that missing view: who is in the room, who
|
|
3009
|
+
* owns it, and the one message every member is meant to see.
|
|
3010
|
+
*
|
|
3011
|
+
* It is a **tab in the official right sidebar**, not a dialog. A roster is something a reader
|
|
3012
|
+
* consults while reading — opening it must not cover the conversation it describes — and the
|
|
3013
|
+
* shell already owns tab placement, width, persistence and the ability to put it side by side
|
|
3014
|
+
* with the document preview. Drawing our own overlay would have to track all of that.
|
|
3015
|
+
*
|
|
3016
|
+
* What the panel shows comes from the shared store rather than its own requests: the sidebar
|
|
3017
|
+
* already reads the roster, and a second reader would let the two disagree about who is present.
|
|
3018
|
+
* The one exception is writing the announcement, which no other surface can do.
|
|
3019
|
+
*
|
|
3020
|
+
* @module @sparkelf/dsh-group-chat/client/MembersPanel
|
|
3021
|
+
*/
|
|
3022
|
+
/** The tab type this panel is opened as. */
|
|
3023
|
+
const MEMBERS_TAB_KIND = "groupChatMembers";
|
|
3024
|
+
/**
|
|
3025
|
+
* Render the roster and announcement for the room this Session belongs to.
|
|
3026
|
+
*
|
|
3027
|
+
* @param props - the Session, copy, and shared store.
|
|
3028
|
+
* @returns the panel, or a short notice when this Session is not a room.
|
|
3029
|
+
*/
|
|
3030
|
+
function MembersPanel(props) {
|
|
3031
|
+
const { sessionId, t, store } = props;
|
|
3032
|
+
const roomId = roomIdOfSession({ id: sessionId });
|
|
3033
|
+
const state = useRoomState(store);
|
|
3034
|
+
(0, react.useEffect)(() => {
|
|
3035
|
+
if (roomId !== void 0) store.loadMembers(roomId);
|
|
3036
|
+
}, [roomId, store]);
|
|
3037
|
+
const members = roomId === void 0 ? void 0 : state.members[roomId];
|
|
3038
|
+
const rows = (0, react.useMemo)(() => members === void 0 ? void 0 : memberRows(members, state.selfId), [members, state.selfId]);
|
|
3039
|
+
const room = state.rooms?.find((candidate) => candidate.id === roomId);
|
|
3040
|
+
if (roomId === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3041
|
+
className: MembersPanel_module_css_default["notice"],
|
|
3042
|
+
children: t("membersNotARoom")
|
|
3043
|
+
});
|
|
3044
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3045
|
+
className: MembersPanel_module_css_default["root"],
|
|
3046
|
+
children: [
|
|
3047
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Announcement, {
|
|
3048
|
+
t,
|
|
3049
|
+
roomId,
|
|
3050
|
+
announcement: room?.announcement,
|
|
3051
|
+
canEdit: room !== void 0 && room.ownerUserId === state.selfId,
|
|
3052
|
+
onSaved: () => {
|
|
3053
|
+
store.refresh();
|
|
3054
|
+
}
|
|
3055
|
+
}),
|
|
3056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SharedFiles, {
|
|
3057
|
+
t,
|
|
3058
|
+
roomId
|
|
3059
|
+
}),
|
|
3060
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Roster, {
|
|
3061
|
+
t,
|
|
3062
|
+
rows,
|
|
3063
|
+
canRemove: room !== void 0 && room.ownerUserId === state.selfId,
|
|
3064
|
+
onRemove: (userId) => {
|
|
3065
|
+
store.removeMember(roomId, userId);
|
|
3066
|
+
}
|
|
3067
|
+
})
|
|
3068
|
+
]
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Subscribe to the shared store.
|
|
3073
|
+
*
|
|
3074
|
+
* Written as a hook rather than read inline so a roster that arrives after the first paint
|
|
3075
|
+
* re-renders the panel: the store is a plain observable, not a framework store.
|
|
3076
|
+
*
|
|
3077
|
+
* @param store - the shared room store.
|
|
3078
|
+
* @returns its current state, re-read on every change.
|
|
3079
|
+
*/
|
|
3080
|
+
function useRoomState(store) {
|
|
3081
|
+
const [snapshot, setSnapshot] = (0, react.useState)(() => store.getState());
|
|
3082
|
+
(0, react.useEffect)(() => {
|
|
3083
|
+
setSnapshot(store.getState());
|
|
3084
|
+
return store.subscribe(() => {
|
|
3085
|
+
setSnapshot(store.getState());
|
|
3086
|
+
});
|
|
3087
|
+
}, [store]);
|
|
3088
|
+
return snapshot;
|
|
3089
|
+
}
|
|
3090
|
+
/**
|
|
3091
|
+
* The announcement area: read it, and for the owner, write it.
|
|
3092
|
+
*
|
|
3093
|
+
* Saving re-reads the room list instead of patching local state. The server stores a cleared
|
|
3094
|
+
* announcement as `null`, so a local patch would leave this panel and the sidebar showing
|
|
3095
|
+
* different spellings of the same absence.
|
|
3096
|
+
*
|
|
3097
|
+
* @param props - the text, the permission, and how to report a save.
|
|
3098
|
+
* @returns the section.
|
|
3099
|
+
*/
|
|
3100
|
+
function Announcement(props) {
|
|
3101
|
+
const { t, roomId, announcement, canEdit, onSaved } = props;
|
|
3102
|
+
const view = announcementView(announcement);
|
|
3103
|
+
const [editing, setEditing] = (0, react.useState)(false);
|
|
3104
|
+
const [draft, setDraft] = (0, react.useState)("");
|
|
3105
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
3106
|
+
const [missing, setMissing] = (0, react.useState)(false);
|
|
3107
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
3108
|
+
const begin = (0, react.useCallback)(() => {
|
|
3109
|
+
setDraft(view.state === "text" ? view.text : "");
|
|
3110
|
+
setFailure(void 0);
|
|
3111
|
+
setEditing(true);
|
|
3112
|
+
}, [view]);
|
|
3113
|
+
const save = (0, react.useCallback)(async () => {
|
|
3114
|
+
setSaving(true);
|
|
3115
|
+
setFailure(void 0);
|
|
3116
|
+
try {
|
|
3117
|
+
await setAnnouncement(roomId, draft.trim());
|
|
3118
|
+
setEditing(false);
|
|
3119
|
+
onSaved();
|
|
3120
|
+
} catch (error) {
|
|
3121
|
+
if (announcementMissing(error)) setMissing(true);
|
|
3122
|
+
else setFailure("announcementFailed");
|
|
3123
|
+
} finally {
|
|
3124
|
+
setSaving(false);
|
|
3125
|
+
}
|
|
3126
|
+
}, [
|
|
3127
|
+
draft,
|
|
3128
|
+
onSaved,
|
|
3129
|
+
roomId
|
|
3130
|
+
]);
|
|
3131
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
|
|
3132
|
+
t,
|
|
3133
|
+
title: t("announcementTitle"),
|
|
3134
|
+
actions: canEdit && !editing && !missing && view.state !== "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3135
|
+
type: "button",
|
|
3136
|
+
className: MembersPanel_module_css_default["linkButton"],
|
|
3137
|
+
onClick: begin,
|
|
3138
|
+
children: view.state === "empty" ? t("announcementAdd") : t("announcementEdit")
|
|
3139
|
+
}) : void 0,
|
|
3140
|
+
children: [missing || view.state === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3141
|
+
className: MembersPanel_module_css_default["unavailable"],
|
|
3142
|
+
children: t("announcementUnavailable")
|
|
3143
|
+
}) : editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3144
|
+
className: MembersPanel_module_css_default["editor"],
|
|
3145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3146
|
+
className: MembersPanel_module_css_default["textarea"],
|
|
3147
|
+
value: draft,
|
|
3148
|
+
placeholder: t("announcementPlaceholder"),
|
|
3149
|
+
onChange: (event) => {
|
|
3150
|
+
setDraft(event.target.value);
|
|
3151
|
+
},
|
|
3152
|
+
onKeyDown: (event) => {
|
|
3153
|
+
if (event.key === "Escape") {
|
|
3154
|
+
event.preventDefault();
|
|
3155
|
+
setEditing(false);
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
3159
|
+
event.preventDefault();
|
|
3160
|
+
save();
|
|
3161
|
+
}
|
|
3162
|
+
},
|
|
3163
|
+
rows: 4
|
|
3164
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3165
|
+
className: MembersPanel_module_css_default["editorFooter"],
|
|
3166
|
+
children: [
|
|
3167
|
+
saving ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3168
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3169
|
+
children: t("announcementSaving")
|
|
3170
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3171
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3172
|
+
children: t("announcementCleared")
|
|
3173
|
+
}),
|
|
3174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3175
|
+
size: "sm",
|
|
3176
|
+
onClick: () => {
|
|
3177
|
+
setEditing(false);
|
|
3178
|
+
},
|
|
3179
|
+
children: t("cancel")
|
|
3180
|
+
}),
|
|
3181
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3182
|
+
size: "sm",
|
|
3183
|
+
variant: "primary",
|
|
3184
|
+
onClick: () => void save(),
|
|
3185
|
+
disabled: saving,
|
|
3186
|
+
children: t("save")
|
|
3187
|
+
})
|
|
3188
|
+
]
|
|
3189
|
+
})]
|
|
3190
|
+
}) : view.state === "text" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3191
|
+
className: MembersPanel_module_css_default["announcementText"],
|
|
3192
|
+
children: view.text
|
|
3193
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3194
|
+
className: MembersPanel_module_css_default["unavailable"],
|
|
3195
|
+
children: canEdit ? t("announcementEmpty") : t("announcementOwnerOnly")
|
|
3196
|
+
}), failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3197
|
+
className: MembersPanel_module_css_default["error"],
|
|
3198
|
+
children: t(failure)
|
|
3199
|
+
})]
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* A collapsible section header.
|
|
3204
|
+
*
|
|
3205
|
+
* The right sidebar can be dragged narrow, and this panel stacks three sections in it. A
|
|
3206
|
+
* section that stays open at 200px squeezes the roster until names collide, so each section
|
|
3207
|
+
* can collapse to its title — the title row is the thinnest thing that still tells a member
|
|
3208
|
+
* what exists below. Collapsed state is local: a member who folded "shared files" to make
|
|
3209
|
+
* room for the roster is describing what they are looking at now, not a preference to
|
|
3210
|
+
* persist across rooms.
|
|
3211
|
+
*
|
|
3212
|
+
* @param props - visible title, optional count, and the body to hide.
|
|
3213
|
+
* @returns the section.
|
|
3214
|
+
*/
|
|
3215
|
+
function CollapsibleSection(props) {
|
|
3216
|
+
const { title, count, defaultOpen = true, actions, t, children } = props;
|
|
3217
|
+
const [open, setOpen] = (0, react.useState)(defaultOpen);
|
|
3218
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3219
|
+
className: MembersPanel_module_css_default["section"],
|
|
3220
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3221
|
+
className: MembersPanel_module_css_default["sectionHeader"],
|
|
3222
|
+
children: [
|
|
3223
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3224
|
+
type: "button",
|
|
3225
|
+
className: MembersPanel_module_css_default["sectionToggle"],
|
|
3226
|
+
"aria-expanded": open,
|
|
3227
|
+
onClick: () => {
|
|
3228
|
+
setOpen((value) => !value);
|
|
3229
|
+
},
|
|
3230
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3231
|
+
className: MembersPanel_module_css_default["caret"],
|
|
3232
|
+
"aria-hidden": "true",
|
|
3233
|
+
children: open ? "▾" : "▸"
|
|
3234
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3235
|
+
className: MembersPanel_module_css_default["sectionLabel"],
|
|
3236
|
+
children: title
|
|
3237
|
+
})]
|
|
3238
|
+
}),
|
|
3239
|
+
count !== void 0 && count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3240
|
+
className: MembersPanel_module_css_default["count"],
|
|
3241
|
+
children: count
|
|
3242
|
+
}),
|
|
3243
|
+
actions
|
|
3244
|
+
]
|
|
3245
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3246
|
+
className: MembersPanel_module_css_default["sectionBody"],
|
|
3247
|
+
children
|
|
3248
|
+
})]
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* The room's shared folder, as the members panel shows it.
|
|
3253
|
+
*
|
|
3254
|
+
* Read on demand rather than kept in the shared store: the folder changes when someone
|
|
3255
|
+
* uploads or when the room's agent writes a file, and a panel that only refreshes on room
|
|
3256
|
+
* switch would show a stale list for the whole time it is open. Re-reading each time the
|
|
3257
|
+
* panel opens keeps the list honest without a poll of its own.
|
|
3258
|
+
*
|
|
3259
|
+
* @param props - the room and copy.
|
|
3260
|
+
* @returns the section.
|
|
3261
|
+
*/
|
|
3262
|
+
function SharedFiles(props) {
|
|
3263
|
+
const { t, roomId } = props;
|
|
3264
|
+
const [state, setState] = (0, react.useState)(void 0);
|
|
3265
|
+
const [preview, setPreview] = (0, react.useState)(void 0);
|
|
3266
|
+
const [previewFailure, setPreviewFailure] = (0, react.useState)(void 0);
|
|
3267
|
+
const [uploading, setUploading] = (0, react.useState)(false);
|
|
3268
|
+
const [uploadFailure, setUploadFailure] = (0, react.useState)(void 0);
|
|
3269
|
+
const [reloadKey, setReloadKey] = (0, react.useState)(0);
|
|
3270
|
+
const [subPath, setSubPath] = (0, react.useState)("");
|
|
3271
|
+
const fileInput = (0, react.useRef)(null);
|
|
3272
|
+
(0, react.useEffect)(() => {
|
|
3273
|
+
let live = true;
|
|
3274
|
+
listRoomFiles(roomId, subPath).then((next) => {
|
|
3275
|
+
if (live) setState(next);
|
|
3276
|
+
}).catch(() => {
|
|
3277
|
+
if (live) setState({
|
|
3278
|
+
path: null,
|
|
3279
|
+
files: []
|
|
3280
|
+
});
|
|
3281
|
+
});
|
|
3282
|
+
return () => {
|
|
3283
|
+
live = false;
|
|
3284
|
+
};
|
|
3285
|
+
}, [
|
|
3286
|
+
roomId,
|
|
3287
|
+
reloadKey,
|
|
3288
|
+
subPath
|
|
3289
|
+
]);
|
|
3290
|
+
if (state === void 0) return null;
|
|
3291
|
+
const crumbs = state.crumbs ?? [];
|
|
3292
|
+
if (state.path === null) return null;
|
|
3293
|
+
const openPreview = (path) => {
|
|
3294
|
+
setPreviewFailure(void 0);
|
|
3295
|
+
getRoomFile(roomId, path).then((next) => {
|
|
3296
|
+
setPreview(next);
|
|
3297
|
+
}).catch(() => {
|
|
3298
|
+
setPreviewFailure(t("membersFileUnreadable"));
|
|
3299
|
+
});
|
|
3300
|
+
};
|
|
3301
|
+
const upload = (picked) => {
|
|
3302
|
+
setUploading(true);
|
|
3303
|
+
setUploadFailure(void 0);
|
|
3304
|
+
uploadRoomFile(roomId, picked).then(() => {
|
|
3305
|
+
setReloadKey((value) => value + 1);
|
|
3306
|
+
}).catch(() => {
|
|
3307
|
+
setUploadFailure(t("membersFileUploadFailed"));
|
|
3308
|
+
}).finally(() => {
|
|
3309
|
+
setUploading(false);
|
|
3310
|
+
if (fileInput.current !== null) fileInput.current.value = "";
|
|
3311
|
+
});
|
|
3312
|
+
};
|
|
3313
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
|
|
3314
|
+
t,
|
|
3315
|
+
title: t("membersFilesTitle"),
|
|
3316
|
+
count: state.files.length,
|
|
3317
|
+
actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
3319
|
+
ref: fileInput,
|
|
3320
|
+
type: "file",
|
|
3321
|
+
className: MembersPanel_module_css_default["filePicker"],
|
|
3322
|
+
"aria-label": t("membersFileUpload"),
|
|
3323
|
+
onChange: (event) => {
|
|
3324
|
+
const picked = event.target.files?.[0];
|
|
3325
|
+
if (picked !== void 0) upload(picked);
|
|
3326
|
+
}
|
|
3327
|
+
}),
|
|
3328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3329
|
+
type: "button",
|
|
3330
|
+
className: MembersPanel_module_css_default["refresh"],
|
|
3331
|
+
"aria-label": t("membersFileUpload"),
|
|
3332
|
+
title: t("membersFileUpload"),
|
|
3333
|
+
disabled: uploading,
|
|
3334
|
+
onClick: () => {
|
|
3335
|
+
fileInput.current?.click();
|
|
3336
|
+
},
|
|
3337
|
+
children: uploading ? "⋯" : "+"
|
|
3338
|
+
}),
|
|
3339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3340
|
+
type: "button",
|
|
3341
|
+
className: MembersPanel_module_css_default["refresh"],
|
|
3342
|
+
"aria-label": t("membersFilesRefresh"),
|
|
3343
|
+
title: t("membersFilesRefresh"),
|
|
3344
|
+
onClick: () => {
|
|
3345
|
+
setReloadKey((value) => value + 1);
|
|
3346
|
+
},
|
|
3347
|
+
children: "↻"
|
|
3348
|
+
})
|
|
3349
|
+
] }),
|
|
3350
|
+
children: [
|
|
3351
|
+
crumbs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
3352
|
+
className: MembersPanel_module_css_default["crumbs"],
|
|
3353
|
+
"aria-label": t("membersFilesTitle"),
|
|
3354
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3355
|
+
type: "button",
|
|
3356
|
+
className: MembersPanel_module_css_default["crumb"],
|
|
3357
|
+
onClick: () => {
|
|
3358
|
+
setSubPath("");
|
|
3359
|
+
},
|
|
3360
|
+
children: t("membersFilesRoot")
|
|
3361
|
+
}), crumbs.map((crumb) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3362
|
+
className: MembersPanel_module_css_default["crumbGroup"],
|
|
3363
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3364
|
+
className: MembersPanel_module_css_default["crumbSep"],
|
|
3365
|
+
"aria-hidden": "true",
|
|
3366
|
+
children: "/"
|
|
3367
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3368
|
+
type: "button",
|
|
3369
|
+
className: MembersPanel_module_css_default["crumb"],
|
|
3370
|
+
onClick: () => {
|
|
3371
|
+
setSubPath(crumb.path);
|
|
3372
|
+
},
|
|
3373
|
+
children: crumb.name
|
|
3374
|
+
})]
|
|
3375
|
+
}, crumb.path))]
|
|
3376
|
+
}),
|
|
3377
|
+
state.files.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3378
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3379
|
+
children: t("membersFilesEmpty")
|
|
3380
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3381
|
+
className: MembersPanel_module_css_default["list"],
|
|
3382
|
+
children: state.files.map((file) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
3383
|
+
className: MembersPanel_module_css_default["row"],
|
|
3384
|
+
children: file.directory ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3385
|
+
type: "button",
|
|
3386
|
+
className: MembersPanel_module_css_default["fileButton"],
|
|
3387
|
+
onClick: () => {
|
|
3388
|
+
setSubPath(joinPath(subPath, file.path));
|
|
3389
|
+
},
|
|
3390
|
+
title: file.path,
|
|
3391
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3392
|
+
className: MembersPanel_module_css_default["fileIcon"],
|
|
3393
|
+
"aria-hidden": "true",
|
|
3394
|
+
children: "📁"
|
|
3395
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3396
|
+
className: MembersPanel_module_css_default["name"],
|
|
3397
|
+
children: file.name
|
|
3398
|
+
})]
|
|
3399
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3400
|
+
type: "button",
|
|
3401
|
+
className: MembersPanel_module_css_default["fileButton"],
|
|
3402
|
+
onClick: () => {
|
|
3403
|
+
openPreview(joinPath(subPath, file.path));
|
|
3404
|
+
},
|
|
3405
|
+
title: file.path,
|
|
3406
|
+
children: [
|
|
3407
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3408
|
+
className: MembersPanel_module_css_default["fileIcon"],
|
|
3409
|
+
"aria-hidden": "true",
|
|
3410
|
+
children: "📄"
|
|
3411
|
+
}),
|
|
3412
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3413
|
+
className: MembersPanel_module_css_default["name"],
|
|
3414
|
+
children: file.name
|
|
3415
|
+
}),
|
|
3416
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3417
|
+
className: MembersPanel_module_css_default["fileSize"],
|
|
3418
|
+
children: formatBytes(file.size)
|
|
3419
|
+
})
|
|
3420
|
+
]
|
|
3421
|
+
})
|
|
3422
|
+
}, file.path))
|
|
3423
|
+
}),
|
|
3424
|
+
previewFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3425
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3426
|
+
children: previewFailure
|
|
3427
|
+
}),
|
|
3428
|
+
uploadFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3429
|
+
className: MembersPanel_module_css_default["error"],
|
|
3430
|
+
children: uploadFailure
|
|
3431
|
+
}),
|
|
3432
|
+
preview !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3433
|
+
className: MembersPanel_module_css_default["preview"],
|
|
3434
|
+
"aria-label": preview.name,
|
|
3435
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3436
|
+
className: MembersPanel_module_css_default["previewHeader"],
|
|
3437
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3438
|
+
className: MembersPanel_module_css_default["previewTitle"],
|
|
3439
|
+
title: preview.path,
|
|
3440
|
+
children: preview.name
|
|
3441
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3442
|
+
type: "button",
|
|
3443
|
+
className: MembersPanel_module_css_default["previewClose"],
|
|
3444
|
+
"aria-label": t("membersFileClose"),
|
|
3445
|
+
onClick: () => {
|
|
3446
|
+
setPreview(void 0);
|
|
3447
|
+
},
|
|
3448
|
+
children: "×"
|
|
3449
|
+
})]
|
|
3450
|
+
}), preview.image !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3451
|
+
className: MembersPanel_module_css_default["previewImage"],
|
|
3452
|
+
src: preview.image,
|
|
3453
|
+
alt: preview.name
|
|
3454
|
+
}) : preview.textLike ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
3455
|
+
className: MembersPanel_module_css_default["previewBody"],
|
|
3456
|
+
children: preview.text
|
|
3457
|
+
}), preview.truncated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3458
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3459
|
+
children: t("membersFileTruncated")
|
|
3460
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3461
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3462
|
+
children: t("membersFileBinary")
|
|
3463
|
+
})]
|
|
3464
|
+
})
|
|
3465
|
+
]
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
/** Joins a folder-relative path onto the folder currently shown. */
|
|
3469
|
+
const joinPath = (prefix, name) => prefix === "" ? name : `${prefix}/${name}`;
|
|
3470
|
+
/** Bytes as a short human string; the panel has one column, not a table. */
|
|
3471
|
+
const formatBytes = (bytes) => {
|
|
3472
|
+
if (bytes < 1024) return `${String(bytes)} B`;
|
|
3473
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
3474
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
3475
|
+
};
|
|
3476
|
+
/**
|
|
3477
|
+
* The roster: a search box and one row per member.
|
|
3478
|
+
*
|
|
3479
|
+
* The search box appears only once the roster is long enough that scanning is slower than
|
|
3480
|
+
* typing. Below that it would be a control that cannot help.
|
|
3481
|
+
*
|
|
3482
|
+
* @param props - the rows and copy.
|
|
3483
|
+
* @returns the section.
|
|
3484
|
+
*/
|
|
3485
|
+
function Roster(props) {
|
|
3486
|
+
const { t, rows, canRemove, onRemove } = props;
|
|
3487
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
3488
|
+
const [searching, setSearching] = (0, react.useState)(false);
|
|
3489
|
+
const visible = (0, react.useMemo)(() => rows === void 0 ? void 0 : filterMemberRows(rows, query), [rows, query]);
|
|
3490
|
+
const closeSearch = () => {
|
|
3491
|
+
setSearching(false);
|
|
3492
|
+
setQuery("");
|
|
3493
|
+
};
|
|
3494
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
|
|
3495
|
+
t,
|
|
3496
|
+
title: t("membersRosterTitle"),
|
|
3497
|
+
count: rows?.length,
|
|
3498
|
+
actions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3499
|
+
type: "button",
|
|
3500
|
+
className: MembersPanel_module_css_default["searchToggle"],
|
|
3501
|
+
"aria-label": searching ? t("membersSearchClear") : t("membersSearchOpen"),
|
|
3502
|
+
"aria-expanded": searching,
|
|
3503
|
+
title: searching ? t("membersSearchClear") : t("membersSearchOpen"),
|
|
3504
|
+
onClick: () => {
|
|
3505
|
+
if (searching) closeSearch();
|
|
3506
|
+
else setSearching(true);
|
|
3507
|
+
},
|
|
3508
|
+
children: searching ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3509
|
+
width: "14",
|
|
3510
|
+
height: "14",
|
|
3511
|
+
viewBox: "0 0 16 16",
|
|
3512
|
+
fill: "none",
|
|
3513
|
+
stroke: "currentColor",
|
|
3514
|
+
strokeWidth: "1.6",
|
|
3515
|
+
strokeLinecap: "round",
|
|
3516
|
+
"aria-hidden": "true",
|
|
3517
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
|
|
3518
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3519
|
+
width: "14",
|
|
3520
|
+
height: "14",
|
|
3521
|
+
viewBox: "0 0 16 16",
|
|
3522
|
+
fill: "none",
|
|
3523
|
+
stroke: "currentColor",
|
|
3524
|
+
strokeWidth: "1.6",
|
|
3525
|
+
strokeLinecap: "round",
|
|
3526
|
+
"aria-hidden": "true",
|
|
3527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3528
|
+
cx: "7",
|
|
3529
|
+
cy: "7",
|
|
3530
|
+
r: "4.5"
|
|
3531
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M10.5 10.5L14 14" })]
|
|
3532
|
+
})
|
|
3533
|
+
}),
|
|
3534
|
+
children: [searching && rows !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3535
|
+
className: MembersPanel_module_css_default["search"],
|
|
3536
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
3537
|
+
value: query,
|
|
3538
|
+
autoFocus: true,
|
|
3539
|
+
placeholder: t("membersSearchPlaceholder"),
|
|
3540
|
+
"aria-label": t("membersSearch"),
|
|
3541
|
+
onChange: (event) => {
|
|
3542
|
+
setQuery(event.target.value);
|
|
3543
|
+
}
|
|
3544
|
+
})
|
|
3545
|
+
}), visible === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3546
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3547
|
+
children: t("loading")
|
|
3548
|
+
}) : visible.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
3549
|
+
className: MembersPanel_module_css_default["hint"],
|
|
3550
|
+
children: rows !== void 0 && rows.length > 0 ? t("membersSearchEmpty") : t("membersEmpty")
|
|
3551
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
3552
|
+
className: MembersPanel_module_css_default["list"],
|
|
3553
|
+
children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
3554
|
+
className: MembersPanel_module_css_default["row"],
|
|
3555
|
+
children: [
|
|
3556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
|
|
3557
|
+
name: row.name,
|
|
3558
|
+
src: row.avatarUrl,
|
|
3559
|
+
size: "md"
|
|
3560
|
+
}),
|
|
3561
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3562
|
+
className: MembersPanel_module_css_default["name"],
|
|
3563
|
+
title: row.name,
|
|
3564
|
+
children: row.name
|
|
3565
|
+
}),
|
|
3566
|
+
row.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3567
|
+
className: MembersPanel_module_css_default["selfTag"],
|
|
3568
|
+
children: t("membersSelf")
|
|
3569
|
+
}),
|
|
3570
|
+
row.owner && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3571
|
+
className: MembersPanel_module_css_default["ownerTag"],
|
|
3572
|
+
children: t("membersOwner")
|
|
3573
|
+
}),
|
|
3574
|
+
canRemove && !row.self && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
3575
|
+
type: "button",
|
|
3576
|
+
className: MembersPanel_module_css_default["remove"],
|
|
3577
|
+
title: t("removeMember"),
|
|
3578
|
+
"aria-label": t("removeMember"),
|
|
3579
|
+
onClick: () => {
|
|
3580
|
+
onRemove(row.userId);
|
|
3581
|
+
},
|
|
3582
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
3583
|
+
width: "14",
|
|
3584
|
+
height: "14",
|
|
3585
|
+
viewBox: "0 0 16 16",
|
|
3586
|
+
fill: "none",
|
|
3587
|
+
stroke: "currentColor",
|
|
3588
|
+
strokeWidth: "1.6",
|
|
3589
|
+
strokeLinecap: "round",
|
|
3590
|
+
"aria-hidden": "true",
|
|
3591
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M3 4.5h10M6.5 4.5V3h3v1.5M5 4.5l.6 8a1 1 0 0 0 1 1h2.8a1 1 0 0 0 1-1l.6-8" })
|
|
3592
|
+
})
|
|
3593
|
+
})
|
|
3594
|
+
]
|
|
3595
|
+
}, row.userId))
|
|
3596
|
+
})]
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* The chip that labels the members tab in the sidebar's tab strip.
|
|
3601
|
+
*
|
|
3602
|
+
* The sidebar calls this separately from the body so the strip can render without mounting every
|
|
3603
|
+
* placed tab; it therefore needs the label alone, and none of the body's data.
|
|
3604
|
+
*
|
|
3605
|
+
* @param props - the localized copy.
|
|
3606
|
+
* @returns the tab's label.
|
|
3607
|
+
*/
|
|
3608
|
+
function MembersTabTitle(props) {
|
|
3609
|
+
return props.t("membersTitle");
|
|
3610
|
+
}
|
|
3611
|
+
//#endregion
|
|
3612
|
+
//#region \0dsh-css:/root/projects/dsh-plugins-plus/packages/group-chat/src/client/GuideEntry.module.css.mjs
|
|
3613
|
+
const css = "._0Vnsuq_entry{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-bg-layer-1);width:380px;max-width:100%;min-height:56px;color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;align-items:center;gap:14px;padding:14px 20px;display:flex}._0Vnsuq_entry:hover{background:var(--dsw-alias-interactive-bg-hover)}._0Vnsuq_icon{width:26px;height:26px;color:var(--dsw-alias-label-secondary);flex:none;justify-content:center;align-items:center;display:flex}._0Vnsuq_text{flex-direction:column;flex:1;gap:3px;min-width:0;display:flex}._0Vnsuq_title{text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:1.4;overflow:hidden}._0Vnsuq_description{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:11px;line-height:1.4;overflow:hidden}";
|
|
3614
|
+
const tagId = "@sparkelf/dsh-group-chat/GuideEntry.module.css";
|
|
3615
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
3616
|
+
const tag = document.createElement("style");
|
|
3617
|
+
tag.dataset.plugin = "@sparkelf/dsh-group-chat";
|
|
3618
|
+
tag.dataset.pluginCss = tagId;
|
|
3619
|
+
tag.textContent = css;
|
|
3620
|
+
document.head.appendChild(tag);
|
|
3621
|
+
}
|
|
3622
|
+
var GuideEntry_module_css_default = {
|
|
3623
|
+
"title": "_0Vnsuq_title",
|
|
3624
|
+
"text": "_0Vnsuq_text",
|
|
3625
|
+
"icon": "_0Vnsuq_icon",
|
|
3626
|
+
"description": "_0Vnsuq_description",
|
|
3627
|
+
"entry": "_0Vnsuq_entry"
|
|
3628
|
+
};
|
|
3629
|
+
//#endregion
|
|
3630
|
+
//#region src/client/GuideEntry.tsx
|
|
3631
|
+
/**
|
|
3632
|
+
* Draw the members box, or nothing when the open conversation is not a room.
|
|
3633
|
+
*
|
|
3634
|
+
* @param props - the box's copy, its Session, and how to open the page.
|
|
3635
|
+
* @returns the box, or `null` outside a room.
|
|
3636
|
+
*/
|
|
3637
|
+
function MembersGuideEntry(props) {
|
|
3638
|
+
const { title, description, sessionId, onPick } = props;
|
|
3639
|
+
if (roomIdOfSession({ id: sessionId }) === void 0) return null;
|
|
3640
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3641
|
+
type: "button",
|
|
3642
|
+
className: GuideEntry_module_css_default["entry"],
|
|
3643
|
+
"data-group-chat-guide-entry": props.entryId,
|
|
3644
|
+
onClick: () => {
|
|
3645
|
+
onPick?.();
|
|
3646
|
+
},
|
|
3647
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3648
|
+
className: GuideEntry_module_css_default["icon"],
|
|
3649
|
+
"aria-hidden": "true",
|
|
3650
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
3651
|
+
width: "26",
|
|
3652
|
+
height: "26",
|
|
3653
|
+
viewBox: "0 0 24 24",
|
|
3654
|
+
fill: "none",
|
|
3655
|
+
stroke: "currentColor",
|
|
3656
|
+
strokeWidth: "1.7",
|
|
3657
|
+
strokeLinecap: "round",
|
|
3658
|
+
strokeLinejoin: "round",
|
|
3659
|
+
children: [
|
|
3660
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }),
|
|
3661
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
3662
|
+
cx: "9",
|
|
3663
|
+
cy: "7",
|
|
3664
|
+
r: "4"
|
|
3665
|
+
}),
|
|
3666
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M22 21v-2a4 4 0 0 0-3-3.87" }),
|
|
3667
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M16 3.13a4 4 0 0 1 0 7.75" })
|
|
3668
|
+
]
|
|
3669
|
+
})
|
|
3670
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3671
|
+
className: GuideEntry_module_css_default["text"],
|
|
3672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3673
|
+
className: GuideEntry_module_css_default["title"],
|
|
3674
|
+
children: title
|
|
3675
|
+
}), description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3676
|
+
className: GuideEntry_module_css_default["description"],
|
|
3677
|
+
children: description
|
|
3678
|
+
})]
|
|
3679
|
+
})]
|
|
3680
|
+
});
|
|
3681
|
+
}
|
|
3682
|
+
//#endregion
|
|
3683
|
+
//#region src/client/locales.ts
|
|
3684
|
+
/**
|
|
3685
|
+
* Group-chat copy, in the shape every locale-backed DSH client plugin uses: one
|
|
3686
|
+
* English object is the key set, and each other locale is a complete record over
|
|
3687
|
+
* that key set so a missing translation fails to compile.
|
|
3688
|
+
*
|
|
3689
|
+
* @module @sparkelf/dsh-group-chat/client/locales
|
|
3690
|
+
*/
|
|
3691
|
+
/** English group-chat copy. */
|
|
3692
|
+
const en = {
|
|
3693
|
+
nav: "Group chat",
|
|
3694
|
+
title: "Group chat",
|
|
3695
|
+
description: "Rooms this workspace has joined. @ yourself to make the agent answer; every other message is recorded and read, but nothing runs.",
|
|
3696
|
+
loading: "Loading rooms…",
|
|
3697
|
+
retry: "Retry",
|
|
3698
|
+
emptyRooms: "No rooms yet",
|
|
3699
|
+
createRoom: "New room",
|
|
3700
|
+
roomName: "Room name",
|
|
3701
|
+
create: "Create",
|
|
3702
|
+
search: "Search rooms",
|
|
3703
|
+
searchPlaceholder: "Search rooms…",
|
|
3704
|
+
searchClear: "Clear search",
|
|
3705
|
+
searchEmpty: "No room matches",
|
|
3706
|
+
discoverHeading: "Rooms you can join",
|
|
3707
|
+
discoverMembers: "{count} members",
|
|
3708
|
+
joinRoom: "Join",
|
|
3709
|
+
joining: "Joining…",
|
|
3710
|
+
viewOptions: "View options",
|
|
3711
|
+
orderBy: "Sort",
|
|
3712
|
+
orderByUpdated: "Recently active",
|
|
3713
|
+
orderByManual: "Manual",
|
|
3714
|
+
archivedFilter: "Left rooms",
|
|
3715
|
+
archivedHide: "Hide",
|
|
3716
|
+
archivedShow: "Show",
|
|
3717
|
+
copyRoomId: "Copy room ID",
|
|
3718
|
+
left: "Left",
|
|
3719
|
+
memberCountShort: "{count}",
|
|
3720
|
+
unreadAria: "{count} unread",
|
|
3721
|
+
unreadMentionAria: "{count} unread, one mentions you",
|
|
3722
|
+
createTitle: "New room",
|
|
3723
|
+
createName: "Room name",
|
|
3724
|
+
createCwd: "Working directory",
|
|
3725
|
+
createCwdHint: "Determines which files the agent may change. Members of this room can ask the agent to modify anything under this directory.",
|
|
3726
|
+
createPreset: "Agent preset",
|
|
3727
|
+
createPresetHint: "Determines which tools the agent has in this room.",
|
|
3728
|
+
createPresetDefault: "The deployment's default preset",
|
|
3729
|
+
createPresetUnavailable: "The preset list is unavailable; the room will use the deployment default.",
|
|
3730
|
+
createMembers: "Initial members",
|
|
3731
|
+
createMembersHint: "You become the first member. Others can be added after the room exists.",
|
|
3732
|
+
createPolicy: "Who may invite",
|
|
3733
|
+
createVisibility: "Who can find this room",
|
|
3734
|
+
createTrigger: "When the agent replies",
|
|
3735
|
+
createNotice: "These settings control where the room appears in the interface. They are not access control.",
|
|
3736
|
+
menuRename: "Rename",
|
|
3737
|
+
menuInvite: "Invite members",
|
|
3738
|
+
menuSettings: "Room settings",
|
|
3739
|
+
menuMembers: "Manage members",
|
|
3740
|
+
menuTransfer: "Transfer ownership",
|
|
3741
|
+
menuTransferTitle: "Transfer ownership",
|
|
3742
|
+
menuTransferHint: "Choose the member who becomes the owner. You become an ordinary member and can then leave.",
|
|
3743
|
+
menuTransferConfirm: "Transfer",
|
|
3744
|
+
menuTransferEmpty: "There is no other member to transfer to yet. Invite someone first.",
|
|
3745
|
+
menuOwnerCannotLeave: "The owner cannot leave. Transfer ownership, or dissolve the room.",
|
|
3746
|
+
menuDissolve: "Dissolve room",
|
|
3747
|
+
menuDissolveTarget: "Room to dissolve: ",
|
|
3748
|
+
menuDissolveConfirm: "Dissolve this room? Every member loses it, and its messages go with it. This cannot be undone.",
|
|
3749
|
+
menuLeave: "Leave room",
|
|
3750
|
+
menuLeaveConfirm: "Leave this room? The history your agent already recorded stays.",
|
|
3751
|
+
renameTitle: "Rename room",
|
|
3752
|
+
renamePlaceholder: "New name",
|
|
3753
|
+
membersTitle: "Manage members",
|
|
3754
|
+
/** The roster section's own heading; the tab label above already says "Manage members". */
|
|
3755
|
+
membersRosterTitle: "Members",
|
|
3756
|
+
membersSearch: "Search members",
|
|
3757
|
+
membersSearchOpen: "Search members",
|
|
3758
|
+
membersSearchPlaceholder: "Search members…",
|
|
3759
|
+
membersSearchClear: "Clear search",
|
|
3760
|
+
membersSearchEmpty: "No member matches",
|
|
3761
|
+
membersEmpty: "This room has no members yet.",
|
|
3762
|
+
membersError: "The member list could not be read. Retry from the room menu.",
|
|
3763
|
+
announcementTitle: "Announcement",
|
|
3764
|
+
announcementEmpty: "No announcement yet",
|
|
3765
|
+
announcementAdd: "Add announcement",
|
|
3766
|
+
announcementEdit: "Edit",
|
|
3767
|
+
announcementPlaceholder: "Write something every member should see",
|
|
3768
|
+
announcementUnavailable: "Announcements are unavailable in this deployment",
|
|
3769
|
+
announcementSaving: "Saving…",
|
|
3770
|
+
announcementOwnerOnly: "Only the room owner can edit the announcement",
|
|
3771
|
+
announcementCleared: "The announcement was cleared",
|
|
3772
|
+
announcementFailed: "Could not save the announcement. Try again.",
|
|
3773
|
+
membersSelf: "You",
|
|
3774
|
+
membersFilesTitle: "Shared files",
|
|
3775
|
+
membersFilesEmpty: "Nothing shared yet.",
|
|
3776
|
+
membersFilesRefresh: "Refresh files",
|
|
3777
|
+
membersFileClose: "Close preview",
|
|
3778
|
+
membersFileUnreadable: "Could not open that file.",
|
|
3779
|
+
membersFileTruncated: "Only the beginning is shown.",
|
|
3780
|
+
membersFileBinary: "Binary file — no text preview.",
|
|
3781
|
+
membersSectionCollapsed: "Collapsed",
|
|
3782
|
+
membersFileUpload: "Upload a file",
|
|
3783
|
+
membersFileUploadFailed: "Upload failed.",
|
|
3784
|
+
resizeRegion: "Resize the group chat region",
|
|
3785
|
+
membersFilesRoot: "Files",
|
|
3786
|
+
membersOwner: "Owner",
|
|
3787
|
+
membersNotARoom: "This conversation is not a group chat, so it has no members.",
|
|
3788
|
+
membersGuideHint: "Members and the announcement for this room",
|
|
3789
|
+
inviteTitle: "Invite members",
|
|
3790
|
+
invitePlaceholder: "Search DataOps accounts by name or handle",
|
|
3791
|
+
inviteHint: "Only existing DataOps accounts can be added.",
|
|
3792
|
+
inviteSubmit: "Invite",
|
|
3793
|
+
inviteEmpty: "No matching accounts. They may already be in this room.",
|
|
3794
|
+
inviteSearching: "Searching…",
|
|
3795
|
+
inviteRemove: "Remove",
|
|
3796
|
+
inviteSearchLabel: "Search accounts",
|
|
3797
|
+
inviteSelectedCount: "{count} selected",
|
|
3798
|
+
inviteSubmitCount: "Invite ({count})",
|
|
3799
|
+
optionEmpty: "Nothing to choose from.",
|
|
3800
|
+
pickThisDirectory: "Use this directory",
|
|
3801
|
+
settingsTitle: "Room settings",
|
|
3802
|
+
removeMember: "Remove",
|
|
3803
|
+
ownerBadge: "Owner",
|
|
3804
|
+
you: "You",
|
|
3805
|
+
createSubmit: "Create",
|
|
3806
|
+
cancel: "Cancel",
|
|
3807
|
+
members: "Members",
|
|
3808
|
+
memberCount: "members",
|
|
3809
|
+
invite: "Invite",
|
|
3810
|
+
leave: "Leave room",
|
|
3811
|
+
leaveConfirm: "Leave this room? Your own agent keeps the history it already recorded.",
|
|
3812
|
+
owner: "Owner",
|
|
3813
|
+
sendPlaceholder: "Message the room. @ yourself to have your agent answer.",
|
|
3814
|
+
send: "Send",
|
|
3815
|
+
sending: "Sending…",
|
|
3816
|
+
agentBadge: "agent",
|
|
3817
|
+
triggeredHint: "Your agent is answering.",
|
|
3818
|
+
notConfigured: "The group-chat server is not configured, so no rooms can be listed.",
|
|
3819
|
+
unreachable: "The group-chat server did not answer.",
|
|
3820
|
+
credentialMissing: "No workspace credential is available, so rooms cannot be read.",
|
|
3821
|
+
credentialRejected: "The workspace credential was refused. Sign in to DataOps again, then retry.",
|
|
3822
|
+
ownerCannotLeave: "The owner cannot leave. Transfer ownership, or dissolve the room.",
|
|
3823
|
+
policy: "Who may invite",
|
|
3824
|
+
policyAnyone: "Any member",
|
|
3825
|
+
policyAdmin: "Owner only",
|
|
3826
|
+
discoverability: "Discoverability",
|
|
3827
|
+
discoverable: "Listed in search",
|
|
3828
|
+
inviteOnly: "Invite only",
|
|
3829
|
+
trigger: "When the agent replies",
|
|
3830
|
+
triggerMention: "Only when @-mentioned",
|
|
3831
|
+
triggerAlways: "Every message",
|
|
3832
|
+
triggerNever: "Never (record only)",
|
|
3833
|
+
save: "Save",
|
|
3834
|
+
saved: "Saved"
|
|
3835
|
+
};
|
|
3836
|
+
/** Simplified Chinese group-chat copy. */
|
|
3837
|
+
const zh = {
|
|
3838
|
+
nav: "群聊",
|
|
3839
|
+
title: "群聊",
|
|
3840
|
+
description: "本工作区已加入的群聊。@ 自己会触发 agent 应答;其他消息都会记录并进入上下文,但不执行。",
|
|
3841
|
+
loading: "正在加载群聊…",
|
|
3842
|
+
retry: "重试",
|
|
3843
|
+
emptyRooms: "暂无群聊",
|
|
3844
|
+
createRoom: "新建群聊",
|
|
3845
|
+
roomName: "群聊名称",
|
|
3846
|
+
create: "创建",
|
|
3847
|
+
search: "搜索群聊",
|
|
3848
|
+
searchPlaceholder: "搜索群聊…",
|
|
3849
|
+
searchClear: "清除搜索",
|
|
3850
|
+
searchEmpty: "无匹配结果",
|
|
3851
|
+
discoverHeading: "可以加入的群聊",
|
|
3852
|
+
discoverMembers: "{count} 位成员",
|
|
3853
|
+
joinRoom: "加入",
|
|
3854
|
+
joining: "加入中…",
|
|
3855
|
+
viewOptions: "视图选项",
|
|
3856
|
+
orderBy: "排序",
|
|
3857
|
+
orderByUpdated: "最近活跃",
|
|
3858
|
+
orderByManual: "手动",
|
|
3859
|
+
archivedFilter: "已退出的群聊",
|
|
3860
|
+
archivedHide: "隐藏",
|
|
3861
|
+
archivedShow: "显示",
|
|
3862
|
+
copyRoomId: "复制群聊 ID",
|
|
3863
|
+
left: "已退出",
|
|
3864
|
+
memberCountShort: "{count}",
|
|
3865
|
+
unreadAria: "{count} 条未读",
|
|
3866
|
+
unreadMentionAria: "{count} 条未读,其中有 @ 你",
|
|
3867
|
+
createTitle: "新建群聊",
|
|
3868
|
+
createName: "群聊名称",
|
|
3869
|
+
createCwd: "工作目录",
|
|
3870
|
+
createCwdHint: "决定机器人能操作哪些文件。此群成员可以让机器人修改该目录下的任何文件。",
|
|
3871
|
+
createPreset: "Agent preset",
|
|
3872
|
+
createPresetHint: "决定机器人在本群可用的工具能力。",
|
|
3873
|
+
createPresetDefault: "跟随部署默认预设",
|
|
3874
|
+
createPresetUnavailable: "预设列表暂时读取不到;创建后将使用部署默认预设。",
|
|
3875
|
+
createMembers: "初始成员",
|
|
3876
|
+
createMembersHint: "创建者自动成为第一个成员;其余成员可在建群后邀请。",
|
|
3877
|
+
createPolicy: "谁能邀请成员",
|
|
3878
|
+
createVisibility: "谁能在群里发现这个房间",
|
|
3879
|
+
createTrigger: "机器人何时回复",
|
|
3880
|
+
createNotice: "这些设置控制房间在界面上的可见性,不构成访问控制。",
|
|
3881
|
+
menuRename: "重命名",
|
|
3882
|
+
menuInvite: "邀请成员",
|
|
3883
|
+
menuSettings: "群聊设置",
|
|
3884
|
+
menuMembers: "成员管理",
|
|
3885
|
+
menuTransfer: "转让群主",
|
|
3886
|
+
menuTransferTitle: "转让群主",
|
|
3887
|
+
menuTransferHint: "选择一位成员成为新群主。转让后你会变成普通成员,那时就可以退出群聊。",
|
|
3888
|
+
menuTransferConfirm: "转让",
|
|
3889
|
+
menuTransferEmpty: "目前没有其他成员可以转让,请先邀请成员。",
|
|
3890
|
+
menuOwnerCannotLeave: "群主不能退出群聊。请先转让群主,或解散群聊。",
|
|
3891
|
+
menuDissolve: "解散群聊",
|
|
3892
|
+
menuDissolveTarget: "将要解散群聊:",
|
|
3893
|
+
menuDissolveConfirm: "解散这个群聊?所有成员都会失去它,群消息也会一并删除,且无法恢复。",
|
|
3894
|
+
menuLeave: "退出群聊",
|
|
3895
|
+
menuLeaveConfirm: "退出这个群聊?你自己的 agent 已记录的历史会保留。",
|
|
3896
|
+
renameTitle: "重命名群聊",
|
|
3897
|
+
renamePlaceholder: "新名称",
|
|
3898
|
+
membersTitle: "成员管理",
|
|
3899
|
+
membersRosterTitle: "成员",
|
|
3900
|
+
membersSearch: "搜索成员",
|
|
3901
|
+
membersSearchOpen: "搜索成员",
|
|
3902
|
+
membersSearchPlaceholder: "搜索成员…",
|
|
3903
|
+
membersSearchClear: "清除搜索",
|
|
3904
|
+
membersSearchEmpty: "没有匹配的成员",
|
|
3905
|
+
membersEmpty: "这个群还没有成员。",
|
|
3906
|
+
membersError: "成员列表读取失败,可从群聊菜单重试。",
|
|
3907
|
+
announcementTitle: "群公告",
|
|
3908
|
+
announcementEmpty: "暂无群公告",
|
|
3909
|
+
announcementAdd: "+ 添加公告",
|
|
3910
|
+
announcementEdit: "编辑",
|
|
3911
|
+
announcementPlaceholder: "写一条所有成员都能看到的公告",
|
|
3912
|
+
announcementUnavailable: "公告功能暂不可用",
|
|
3913
|
+
announcementSaving: "保存中…",
|
|
3914
|
+
announcementOwnerOnly: "只有群主可以编辑公告",
|
|
3915
|
+
announcementCleared: "公告已清空",
|
|
3916
|
+
announcementFailed: "公告保存失败,请重试。",
|
|
3917
|
+
membersSelf: "我",
|
|
3918
|
+
membersFilesTitle: "群文件",
|
|
3919
|
+
membersFilesEmpty: "还没有共享文件。",
|
|
3920
|
+
membersFilesRefresh: "刷新文件",
|
|
3921
|
+
membersFileClose: "关闭预览",
|
|
3922
|
+
membersFileUnreadable: "无法打开该文件。",
|
|
3923
|
+
membersFileTruncated: "仅显示开头部分。",
|
|
3924
|
+
membersFileBinary: "二进制文件,不提供文本预览。",
|
|
3925
|
+
membersSectionCollapsed: "已折叠",
|
|
3926
|
+
membersFileUpload: "上传文件",
|
|
3927
|
+
membersFileUploadFailed: "上传失败。",
|
|
3928
|
+
resizeRegion: "拖动调节群聊区域高度",
|
|
3929
|
+
membersFilesRoot: "群文件",
|
|
3930
|
+
membersOwner: "群主",
|
|
3931
|
+
membersNotARoom: "该会话不是群聊,没有群成员。",
|
|
3932
|
+
membersGuideHint: "本群成员与群公告",
|
|
3933
|
+
inviteTitle: "邀请成员",
|
|
3934
|
+
invitePlaceholder: "按姓名或账号搜索 DataOps 用户",
|
|
3935
|
+
inviteHint: "只能加入已存在的 DataOps 账号。",
|
|
3936
|
+
inviteEmpty: "没有匹配的账号,可能已在群里。",
|
|
3937
|
+
inviteSearching: "搜索中…",
|
|
3938
|
+
inviteRemove: "移除",
|
|
3939
|
+
inviteSearchLabel: "搜索账号",
|
|
3940
|
+
inviteSelectedCount: "已选 {count} 人",
|
|
3941
|
+
inviteSubmitCount: "邀请 ({count})",
|
|
3942
|
+
optionEmpty: "没有可选项。",
|
|
3943
|
+
pickThisDirectory: "用这个目录",
|
|
3944
|
+
inviteSubmit: "邀请",
|
|
3945
|
+
settingsTitle: "群聊设置",
|
|
3946
|
+
removeMember: "移除",
|
|
3947
|
+
ownerBadge: "群主",
|
|
3948
|
+
you: "我",
|
|
3949
|
+
createSubmit: "创建",
|
|
3950
|
+
cancel: "取消",
|
|
3951
|
+
members: "成员",
|
|
3952
|
+
memberCount: "位成员",
|
|
3953
|
+
invite: "邀请",
|
|
3954
|
+
leave: "退出群聊",
|
|
3955
|
+
leaveConfirm: "退出这个群聊?你自己的 agent 会保留已记录的历史。",
|
|
3956
|
+
owner: "群主",
|
|
3957
|
+
sendPlaceholder: "发消息。@ 自己可以让你的 agent 应答。",
|
|
3958
|
+
send: "发送",
|
|
3959
|
+
sending: "发送中…",
|
|
3960
|
+
agentBadge: "agent",
|
|
3961
|
+
triggeredHint: "你的 agent 正在应答。",
|
|
3962
|
+
notConfigured: "未配置群聊服务器,无法列出群聊。",
|
|
3963
|
+
unreachable: "群聊服务器没有响应。",
|
|
3964
|
+
credentialMissing: "没有可用的工作区凭证,无法读取群聊。",
|
|
3965
|
+
credentialRejected: "工作区凭证被拒绝。请重新登录 DataOps 后重试。",
|
|
3966
|
+
ownerCannotLeave: "群主不能退出群聊。请先转让群主,或解散群聊。",
|
|
3967
|
+
policy: "谁可以邀请",
|
|
3968
|
+
policyAnyone: "任何成员",
|
|
3969
|
+
policyAdmin: "仅群主",
|
|
3970
|
+
discoverability: "可发现性",
|
|
3971
|
+
discoverable: "可在搜索中列出",
|
|
3972
|
+
inviteOnly: "仅限邀请",
|
|
3973
|
+
trigger: "机器人何时回复",
|
|
3974
|
+
triggerMention: "仅被 @ 时回复",
|
|
3975
|
+
triggerAlways: "每条都回复",
|
|
3976
|
+
triggerNever: "从不回复(纯记录)",
|
|
3977
|
+
save: "保存",
|
|
3978
|
+
saved: "已保存"
|
|
3979
|
+
};
|
|
3980
|
+
//#endregion
|
|
3981
|
+
//#region src/client/index.ts
|
|
3982
|
+
const NS = "settings.groupChat";
|
|
3983
|
+
/**
|
|
3984
|
+
* Every `user`-shaped cell of the chat-node seat.
|
|
3985
|
+
*
|
|
3986
|
+
* `user` and `steering` are separate cells that render the *same* official component: a room
|
|
3987
|
+
* message is `steering` once a turn has claimed it and `user` otherwise, so shadowing only
|
|
3988
|
+
* `user` would leave exactly the messages that arrived while the agent was working on the
|
|
3989
|
+
* right. Both are cell-local shadows, so neither reaches an ordinary Session: the side is
|
|
3990
|
+
* decided from the message's own room source, and an ordinary message has none.
|
|
3991
|
+
*/
|
|
3992
|
+
const CHAT_NODE_KEYS = ["user", "steering"];
|
|
3993
|
+
/** Priority this plugin claims its chat-node cells at; the official entries sit at `0`. */
|
|
3994
|
+
const SHADOW_PRIORITY = -1;
|
|
3995
|
+
/**
|
|
3996
|
+
* This plugin's own chat-node renderer, so the lookup below cannot mistake it for the
|
|
3997
|
+
* official one. Compared by identity against the registered component.
|
|
3998
|
+
*/
|
|
3999
|
+
const OUR_VIEW = MessageSide;
|
|
4000
|
+
/**
|
|
4001
|
+
* The entry the shell would render for one cell without this plugin.
|
|
4002
|
+
*
|
|
4003
|
+
* The registry projects shadowing winners through `entriesOfSlot`, which is exactly the
|
|
4004
|
+
* wrong question here — this plugin is the winner. `entries` keeps the whole ledger, so the
|
|
4005
|
+
* official entry is still visible, and picking the lowest-priority live one other than this
|
|
4006
|
+
* plugin's own reproduces the pre-plugin choice.
|
|
4007
|
+
*
|
|
4008
|
+
* @param ledger - every registered entry for the chat-node slot.
|
|
4009
|
+
* @param key - the cell being claimed.
|
|
4010
|
+
* @param ours - this plugin's own component, excluded from the candidates.
|
|
4011
|
+
* @returns the official component, or `undefined` when nothing else is registered.
|
|
4012
|
+
*/
|
|
4013
|
+
const officialViewFor = (ledger, key, ours) => ledger.filter((entry) => entry.options.key === key && entry.component !== ours).reduce((lowest, entry) => lowest === void 0 || (entry.options.priority ?? 0) < (lowest.options.priority ?? 0) ? entry : lowest, void 0)?.component;
|
|
4014
|
+
/**
|
|
4015
|
+
* Report why one chat-node cell could not be claimed.
|
|
4016
|
+
*
|
|
4017
|
+
* A rejected claim is not fatal — the official renderer still draws that cell — but it does
|
|
4018
|
+
* mean the side never flips, which is invisible in the UI. The console is the only channel
|
|
4019
|
+
* this half has, and the message has to name the cell so the missing side can be traced to it.
|
|
4020
|
+
*/
|
|
4021
|
+
const logClaimFailure = (key, error) => {
|
|
4022
|
+
console.warn(`[group-chat] could not claim the "${key}" chat-node cell; messages of this kind stay on the official side`, error);
|
|
4023
|
+
};
|
|
4024
|
+
/** Main-panel key the sidebar rows select. */
|
|
4025
|
+
const PANEL_ID = "group-chat";
|
|
4026
|
+
/**
|
|
4027
|
+
* The members tab type.
|
|
4028
|
+
*
|
|
4029
|
+
* `MEMBERS_TAB_ID` keys the type's slot entries; `MEMBERS_TAB_KIND` is what `openTab` resolves.
|
|
4030
|
+
* They are separate values because the sidebar stores a placed tab by kind while the slot ledger
|
|
4031
|
+
* is keyed by id — the same split the official schedule plugin makes.
|
|
4032
|
+
*/
|
|
4033
|
+
const MEMBERS_TAB_ID = "@sparkelf/dsh-group-chat/members";
|
|
4034
|
+
/**
|
|
4035
|
+
* The guide entry's own identity, and where it sorts on the start page.
|
|
4036
|
+
*
|
|
4037
|
+
* An id rather than the tab's: the guide lists one box per entry, and the id is what the
|
|
4038
|
+
* entry-level slot replacement below is keyed by. The order lands the box among the other
|
|
4039
|
+
* `extension` types instead of at either end.
|
|
4040
|
+
*/
|
|
4041
|
+
const MEMBERS_GUIDE_ID = "@sparkelf/dsh-group-chat/members-guide";
|
|
4042
|
+
const MEMBERS_GUIDE_ORDER = 60;
|
|
4043
|
+
const inject = [
|
|
4044
|
+
"slots",
|
|
4045
|
+
"locale",
|
|
4046
|
+
"uiWorkspace",
|
|
4047
|
+
"workspaces",
|
|
4048
|
+
"sidebarRightTabs",
|
|
4049
|
+
"sidebarRight"
|
|
4050
|
+
];
|
|
4051
|
+
/**
|
|
4052
|
+
* Register the sidebar region and the room page.
|
|
4053
|
+
* @param ctx - DSH client context with locale and slot registries.
|
|
4054
|
+
*/
|
|
4055
|
+
function apply(ctx) {
|
|
4056
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
4057
|
+
zh,
|
|
4058
|
+
en
|
|
4059
|
+
}), "group-chat: copy");
|
|
4060
|
+
const t = ctx.locale.bind(NS);
|
|
4061
|
+
const tPreset = ctx.locale.bind(PRESET_LOCALE_NS);
|
|
4062
|
+
const base = createRoomStore();
|
|
4063
|
+
const store = {
|
|
4064
|
+
...base,
|
|
4065
|
+
open: (roomId) => {
|
|
4066
|
+
base.open(roomId);
|
|
4067
|
+
ctx.uiWorkspace.openSession(roomSessionId(roomId));
|
|
4068
|
+
queueMicrotask(() => {
|
|
4069
|
+
store.openMembers?.(roomId);
|
|
4070
|
+
});
|
|
4071
|
+
},
|
|
4072
|
+
/**
|
|
4073
|
+
* Dissolve a room, then take its Session off the shell's grouping surfaces.
|
|
4074
|
+
*
|
|
4075
|
+
* The room row disappears because the server deleted it, but the Session is a durable
|
|
4076
|
+
* record that outlives the room: `dissolveRoom` cannot reach it, so without this the
|
|
4077
|
+
* room's leftover workspace (from before rooms stopped registering one) and its Session
|
|
4078
|
+
* entry would stay listed for a conversation that no longer exists. Archiving is the
|
|
4079
|
+
* documented, reversible way to remove a Session from those surfaces — the same action
|
|
4080
|
+
* the shell's own "archive session" uses.
|
|
4081
|
+
*
|
|
4082
|
+
* Runs after the delete so a refused dissolve leaves the Session alone.
|
|
4083
|
+
*/
|
|
4084
|
+
dissolve: async (roomId) => {
|
|
4085
|
+
await base.dissolve(roomId);
|
|
4086
|
+
try {
|
|
4087
|
+
await ctx.workspaces.archiveSession(roomSessionId(roomId), { stopActivity: true });
|
|
4088
|
+
const sessionId = roomSessionId(roomId);
|
|
4089
|
+
const snapshot = ctx.workspaces.list.getSnapshot();
|
|
4090
|
+
for (const workspace of snapshot?.items ?? []) {
|
|
4091
|
+
if (!workspace.sessionIds.includes(sessionId)) continue;
|
|
4092
|
+
await ctx.workspaces.delete(workspace.workspaceId);
|
|
4093
|
+
}
|
|
4094
|
+
} catch {}
|
|
4095
|
+
},
|
|
4096
|
+
/**
|
|
4097
|
+
* Open the members panel for a room.
|
|
4098
|
+
*
|
|
4099
|
+
* Two steps, in this order and not the other way round: the room has to be the *mounted*
|
|
4100
|
+
* Session before a right-sidebar tab can be placed beside it, because `openTab` acts on the
|
|
4101
|
+
* mounted seat and throws when there is none. `openSession` is synchronous in what it records
|
|
4102
|
+
* but the seat re-binds on the next frame, so the tab is opened on a zero-delay task rather
|
|
4103
|
+
* than inline — inline would aim at the session the operator is leaving.
|
|
4104
|
+
*/
|
|
4105
|
+
openMembers: (roomId) => {
|
|
4106
|
+
const open = () => {
|
|
4107
|
+
ctx.sidebarRight.openTab(MEMBERS_TAB_KIND, { params: { roomId } });
|
|
4108
|
+
};
|
|
4109
|
+
if (base.getState().activeRoomId === roomId) open();
|
|
4110
|
+
else setTimeout(open, 0);
|
|
4111
|
+
}
|
|
4112
|
+
};
|
|
4113
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
4114
|
+
name: "sidebar.footer.action",
|
|
4115
|
+
id: PANEL_ID,
|
|
4116
|
+
order: 20,
|
|
4117
|
+
inject: () => ({
|
|
4118
|
+
t,
|
|
4119
|
+
tPreset,
|
|
4120
|
+
store
|
|
4121
|
+
})
|
|
4122
|
+
}, SidebarRooms));
|
|
4123
|
+
/**
|
|
4124
|
+
* The members panel, as a tab in the official right sidebar.
|
|
4125
|
+
*
|
|
4126
|
+
* Three registrations, because the sidebar keeps three separate facts about a tab: the type it
|
|
4127
|
+
* belongs to (so navigation can resolve and persist it), the body to draw, and the chip that
|
|
4128
|
+
* labels it. `id` identifies the type's slot entries; `kind` is what `openTab` takes. They are
|
|
4129
|
+
* not the same value — the official schedule plugin uses
|
|
4130
|
+
* `"@deepseek-ai/dsh-client-ui-schedule/task"` and `"scheduleTask"` respectively — and
|
|
4131
|
+
* collapsing them would make `openTab` throw where nobody can see it.
|
|
4132
|
+
*
|
|
4133
|
+
* The panel is registered for every profile and decides at render time whether the Session it
|
|
4134
|
+
* was opened beside is a room. Registering conditionally instead would lose a placed tab on the
|
|
4135
|
+
* next reload: a tab whose type is gone cannot be restored.
|
|
4136
|
+
*/
|
|
4137
|
+
/**
|
|
4138
|
+
* Open this plugin's page type from the start guide.
|
|
4139
|
+
*
|
|
4140
|
+
* The guide's shipped box opens through the guide's own `openTab`; a replacement box is not
|
|
4141
|
+
* handed that callback, so it goes through the same service the plugin already holds.
|
|
4142
|
+
*/
|
|
4143
|
+
const openTabFromGuide = (kind) => {
|
|
4144
|
+
try {
|
|
4145
|
+
ctx.sidebarRight.openTab(kind);
|
|
4146
|
+
} catch (error) {
|
|
4147
|
+
console.error("[group-chat] opening the members page failed", error);
|
|
4148
|
+
}
|
|
4149
|
+
};
|
|
4150
|
+
ctx.effect(() => ctx.sidebarRightTabs.register({
|
|
4151
|
+
id: MEMBERS_TAB_ID,
|
|
4152
|
+
kind: MEMBERS_TAB_KIND,
|
|
4153
|
+
priority: "extension",
|
|
4154
|
+
title: () => t("membersTitle"),
|
|
4155
|
+
guide: [{
|
|
4156
|
+
id: MEMBERS_GUIDE_ID,
|
|
4157
|
+
order: MEMBERS_GUIDE_ORDER,
|
|
4158
|
+
title: () => t("membersTitle"),
|
|
4159
|
+
description: () => t("membersGuideHint")
|
|
4160
|
+
}]
|
|
4161
|
+
}), "group-chat: members tab type");
|
|
4162
|
+
ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab", () => ctx.slots.register({
|
|
4163
|
+
name: "sidebar.right.pane.tab",
|
|
4164
|
+
key: MEMBERS_TAB_ID,
|
|
4165
|
+
locale: NS,
|
|
4166
|
+
inject: ((sessionId) => ({
|
|
4167
|
+
sessionId,
|
|
4168
|
+
t,
|
|
4169
|
+
store
|
|
4170
|
+
}))
|
|
4171
|
+
}, MembersPanel)), "group-chat: members tab body");
|
|
4172
|
+
ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({
|
|
4173
|
+
name: "sidebar.right.pane.tab.title",
|
|
4174
|
+
key: MEMBERS_TAB_ID,
|
|
4175
|
+
locale: NS,
|
|
4176
|
+
inject: () => ({ t })
|
|
4177
|
+
}, MembersTabTitle)), "group-chat: members tab title");
|
|
4178
|
+
ctx.effect(() => ctx.slots.inject("sidebar.right.tab.guide.entry", () => ctx.slots.register({
|
|
4179
|
+
name: "sidebar.right.tab.guide.entry",
|
|
4180
|
+
key: MEMBERS_TAB_ID,
|
|
4181
|
+
locale: NS,
|
|
4182
|
+
inject: ((sessionId) => ({
|
|
4183
|
+
sessionId,
|
|
4184
|
+
onPick: () => {
|
|
4185
|
+
openTabFromGuide(MEMBERS_TAB_KIND);
|
|
4186
|
+
}
|
|
4187
|
+
}))
|
|
4188
|
+
}, MembersGuideEntry)), "group-chat: members guide box");
|
|
4189
|
+
ctx.slots.inject("conversation.chat.node", () => {
|
|
4190
|
+
let claims = [];
|
|
4191
|
+
const release = () => {
|
|
4192
|
+
for (const dispose of claims) dispose();
|
|
4193
|
+
claims = [];
|
|
4194
|
+
};
|
|
4195
|
+
const reconcile = () => {
|
|
4196
|
+
const ledger = ctx.slots.entries("conversation.chat.node");
|
|
4197
|
+
const claimed = new Set(ledger.filter((entry) => (entry.options.priority ?? 0) === SHADOW_PRIORITY).map((entry) => entry.options.key));
|
|
4198
|
+
for (const key of CHAT_NODE_KEYS) {
|
|
4199
|
+
if (claimed.has(key)) continue;
|
|
4200
|
+
const official = officialViewFor(ledger, key, OUR_VIEW);
|
|
4201
|
+
if (official === void 0) continue;
|
|
4202
|
+
let dispose;
|
|
4203
|
+
try {
|
|
4204
|
+
dispose = ctx.slots.register({
|
|
4205
|
+
name: "conversation.chat.node",
|
|
4206
|
+
key,
|
|
4207
|
+
priority: SHADOW_PRIORITY,
|
|
4208
|
+
locale: "chat"
|
|
4209
|
+
}, (props) => MessageSide({
|
|
4210
|
+
...props,
|
|
4211
|
+
Official: official,
|
|
4212
|
+
getSelfId: store.selfId,
|
|
4213
|
+
subscribeSelf: store.subscribe,
|
|
4214
|
+
getRoster: (roomId) => store.getState().members[roomId],
|
|
4215
|
+
sided: props["node"] !== void 0
|
|
4216
|
+
}));
|
|
4217
|
+
} catch (error) {
|
|
4218
|
+
logClaimFailure(key, error);
|
|
4219
|
+
}
|
|
4220
|
+
if (dispose !== void 0) claims.push(dispose);
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
const unsubscribe = ctx.slots.subscribe("conversation.chat.node", reconcile);
|
|
4224
|
+
reconcile();
|
|
4225
|
+
return () => {
|
|
4226
|
+
unsubscribe();
|
|
4227
|
+
release();
|
|
4228
|
+
};
|
|
4229
|
+
});
|
|
4230
|
+
}
|
|
4231
|
+
//#endregion
|
|
4232
|
+
exports.apply = apply;
|
|
4233
|
+
exports.inject = inject;
|
|
4234
|
+
return module.exports;
|
|
4235
|
+
}
|
|
4236
|
+
});
|
|
4237
|
+
|
|
4238
|
+
//# sourceMappingURL=client.js.map
|