@dropby/react 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1095 @@
1
+ "use client";
2
+ import { t as useDropBy } from "../chunks/use-dropby-BW4iKpiA.js";
3
+ import { a as useChatThread, i as useIdeas, n as useFeatureFlag, o as useChatThreads, r as usePendingAsks } from "../chunks/use-feature-gByQaSp1.js";
4
+ import { n as chatAttachmentMaxByteSize, r as uuid } from "../chunks/chat-attachments-uP-PsyQf.js";
5
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { DropByError, isVotableIdea, validateAskAnswers } from "@dropby/browser";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ import { fetchChatAttachmentFile } from "@dropby/browser/attachments";
9
+ function asDropByError(caught) {
10
+ return caught instanceof DropByError ? caught : new DropByError("request_failed", caught instanceof Error ? caught.message : "DropBy request failed", { cause: caught });
11
+ }
12
+ function paginate(fields, size) {
13
+ const limit = size && size > 0 ? size : fields.length;
14
+ if (limit >= fields.length) return [fields];
15
+ const pages = [];
16
+ for (let i = 0; i < fields.length; i += limit) pages.push(fields.slice(i, i + limit));
17
+ return pages;
18
+ }
19
+ function toRequest(answers) {
20
+ return { answers: Object.entries(answers).map(([fieldId, value]) => ({
21
+ fieldId,
22
+ value
23
+ })) };
24
+ }
25
+ function issuesFor(fields, answers) {
26
+ const map = {};
27
+ for (const issue of validateAskAnswers({ fields }, toRequest(answers)).issues) map[issue.fieldId] = issue.message;
28
+ return map;
29
+ }
30
+ function AskForm({ ask, fieldsPerStep, children, ...rest }) {
31
+ const { actions } = usePendingAsks();
32
+ const [answers, setAnswers] = useState({});
33
+ const [step, setStep] = useState(0);
34
+ const [submitting, setSubmitting] = useState(false);
35
+ const [issues, setIssues] = useState({});
36
+ const [failure, setFailure] = useState(null);
37
+ const pages = useMemo(() => paginate(ask.fields, fieldsPerStep), [ask.fields, fieldsPerStep]);
38
+ const stepCount = pages.length;
39
+ const safeStep = Math.min(step, stepCount - 1);
40
+ const fields = pages[safeStep] ?? [];
41
+ const isFirst = safeStep === 0;
42
+ const isLast = safeStep === stepCount - 1;
43
+ const stepped = stepCount > 1;
44
+ function setAnswer(fieldId, value) {
45
+ setAnswers((current) => ({
46
+ ...current,
47
+ [fieldId]: value
48
+ }));
49
+ setIssues((current) => {
50
+ if (!(fieldId in current)) return current;
51
+ const next = { ...current };
52
+ delete next[fieldId];
53
+ return next;
54
+ });
55
+ }
56
+ function back() {
57
+ setIssues({});
58
+ setStep((value) => Math.max(0, value - 1));
59
+ }
60
+ function next() {
61
+ const stepIssues = issuesFor(fields, answers);
62
+ if (Object.keys(stepIssues).length > 0) {
63
+ setIssues(stepIssues);
64
+ return;
65
+ }
66
+ setIssues({});
67
+ setStep((value) => Math.min(stepCount - 1, value + 1));
68
+ }
69
+ async function submit() {
70
+ const allIssues = issuesFor(ask.fields, answers);
71
+ if (Object.keys(allIssues).length > 0) {
72
+ setIssues(allIssues);
73
+ const firstBadStep = pages.findIndex((page) => page.some((field) => allIssues[field.fieldId]));
74
+ if (firstBadStep >= 0) setStep(firstBadStep);
75
+ return;
76
+ }
77
+ await attempt("submit", () => actions.submit(ask.askId, toRequest(answers)));
78
+ }
79
+ async function dismiss() {
80
+ await attempt("dismiss", () => actions.dismiss(ask.askId));
81
+ }
82
+ async function attempt(action, run) {
83
+ setSubmitting(true);
84
+ setFailure(null);
85
+ try {
86
+ await run();
87
+ } catch (caught) {
88
+ setFailure({
89
+ action,
90
+ error: asDropByError(caught)
91
+ });
92
+ } finally {
93
+ setSubmitting(false);
94
+ }
95
+ }
96
+ function handleSubmit(event) {
97
+ event.preventDefault();
98
+ if (isLast) submit();
99
+ else next();
100
+ }
101
+ const state = {
102
+ ask,
103
+ fields,
104
+ answers,
105
+ setAnswer,
106
+ step: safeStep,
107
+ stepCount,
108
+ isFirst,
109
+ isLast,
110
+ back,
111
+ next,
112
+ submit,
113
+ dismiss,
114
+ submitting,
115
+ issues,
116
+ error: failure?.error ?? null
117
+ };
118
+ if (children) return /* @__PURE__ */ jsx("form", {
119
+ onSubmit: handleSubmit,
120
+ ...rest,
121
+ children: children(state)
122
+ });
123
+ return /* @__PURE__ */ jsxs("form", {
124
+ "data-step": stepped ? safeStep + 1 : void 0,
125
+ "data-step-count": stepped ? stepCount : void 0,
126
+ onSubmit: handleSubmit,
127
+ ...rest,
128
+ children: [
129
+ fields.map((field) => /* @__PURE__ */ jsx(AskFieldControl, {
130
+ field,
131
+ value: answers[field.fieldId],
132
+ issue: issues[field.fieldId],
133
+ onChange: (value) => setAnswer(field.fieldId, value)
134
+ }, field.fieldId)),
135
+ failure ? /* @__PURE__ */ jsx("p", {
136
+ role: "alert",
137
+ children: failure.action === "submit" ? "couldn't send your answer. try again." : "couldn't dismiss this. try again."
138
+ }) : null,
139
+ /* @__PURE__ */ jsxs("div", { children: [
140
+ !isFirst ? /* @__PURE__ */ jsx("button", {
141
+ type: "button",
142
+ onClick: back,
143
+ children: "back"
144
+ }) : null,
145
+ /* @__PURE__ */ jsx("button", {
146
+ type: "submit",
147
+ "aria-busy": submitting || void 0,
148
+ "data-busy": submitting ? "" : void 0,
149
+ disabled: submitting,
150
+ children: isLast ? "submit" : "next"
151
+ }),
152
+ ask.dismissible ? /* @__PURE__ */ jsx("button", {
153
+ type: "button",
154
+ onClick: () => void dismiss(),
155
+ disabled: submitting,
156
+ children: "dismiss"
157
+ }) : null
158
+ ] })
159
+ ]
160
+ });
161
+ }
162
+ function AskFieldControl({ field, value, issue, onChange }) {
163
+ return /* @__PURE__ */ jsxs("div", {
164
+ role: "group",
165
+ "aria-label": field.label,
166
+ "aria-invalid": issue ? true : void 0,
167
+ "data-field-type": field.type,
168
+ children: [
169
+ /* @__PURE__ */ jsxs("span", { children: [field.label, field.required ? " *" : ""] }),
170
+ /* @__PURE__ */ jsx(AskFieldInput, {
171
+ field,
172
+ value,
173
+ onChange
174
+ }),
175
+ issue ? /* @__PURE__ */ jsx("span", {
176
+ role: "alert",
177
+ children: issue
178
+ }) : null
179
+ ]
180
+ });
181
+ }
182
+ function AskFieldInput({ field, value, onChange }) {
183
+ if (field.type === "short_text" || field.type === "long_text") {
184
+ const text = typeof value === "string" ? value : "";
185
+ const props = {
186
+ "aria-label": field.label,
187
+ maxLength: field.maxLength,
188
+ value: text,
189
+ onChange: (event) => onChange(event.target.value)
190
+ };
191
+ return field.type === "long_text" ? /* @__PURE__ */ jsx("textarea", { ...props }) : /* @__PURE__ */ jsx("input", {
192
+ type: "text",
193
+ ...props
194
+ });
195
+ }
196
+ if (field.type === "rating") {
197
+ const ratings = [];
198
+ for (let n = field.min; n <= field.max; n += 1) ratings.push(n);
199
+ return /* @__PURE__ */ jsx(Fragment, { children: ratings.map((n) => /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
200
+ type: "radio",
201
+ name: field.fieldId,
202
+ checked: value === n,
203
+ onChange: () => onChange(n)
204
+ }), n] }, n)) });
205
+ }
206
+ const selected = Array.isArray(value) ? value : [];
207
+ const multiple = field.type === "multi_choice";
208
+ const atMax = multiple && selected.length >= field.maxSelections;
209
+ return /* @__PURE__ */ jsx(Fragment, { children: field.options.map((option) => {
210
+ const checked = selected.includes(option.optionId);
211
+ return /* @__PURE__ */ jsxs("label", { children: [/* @__PURE__ */ jsx("input", {
212
+ type: multiple ? "checkbox" : "radio",
213
+ name: field.fieldId,
214
+ checked,
215
+ disabled: multiple ? !checked && atMax : false,
216
+ onChange: () => {
217
+ if (!multiple) onChange([option.optionId]);
218
+ else onChange(checked ? selected.filter((id) => id !== option.optionId) : [...selected, option.optionId]);
219
+ }
220
+ }), option.label] }, option.optionId);
221
+ }) });
222
+ }
223
+ function Ask({ ask, fieldsPerStep, children, ...rest }) {
224
+ if (children) return /* @__PURE__ */ jsx("div", {
225
+ "data-pending": "",
226
+ ...rest,
227
+ children: children({ ask })
228
+ });
229
+ return /* @__PURE__ */ jsxs("div", {
230
+ "data-pending": "",
231
+ ...rest,
232
+ children: [/* @__PURE__ */ jsx("strong", { children: ask.title }), /* @__PURE__ */ jsx(AskForm, {
233
+ ask,
234
+ ...fieldsPerStep !== void 0 ? { fieldsPerStep } : {}
235
+ })]
236
+ });
237
+ }
238
+ function scrollPort(root) {
239
+ const doc = root.ownerDocument, win = doc.defaultView;
240
+ for (let element = root; element; element = element.parentElement) if (win && element.scrollHeight > element.clientHeight && /(auto|scroll|overlay)/.test(win.getComputedStyle(element).overflowY)) return {
241
+ element,
242
+ target: element,
243
+ top: () => element.getBoundingClientRect().top,
244
+ bottom: () => element.getBoundingClientRect().bottom
245
+ };
246
+ return {
247
+ element: doc.scrollingElement ?? doc.documentElement,
248
+ target: win ?? doc,
249
+ top: () => 0,
250
+ bottom: () => win?.innerHeight ?? doc.documentElement.clientHeight
251
+ };
252
+ }
253
+ function messages(root) {
254
+ return [...root.querySelectorAll("[data-message-id]")];
255
+ }
256
+ function watchChatScrollPosition(root) {
257
+ const port = scrollPort(root);
258
+ let anchorId, offset = 0;
259
+ const capture = () => {
260
+ const anchor = messages(root).find((message) => message.getBoundingClientRect().bottom > port.top());
261
+ anchorId = anchor?.dataset.messageId;
262
+ offset = anchor ? anchor.getBoundingClientRect().top - port.top() : 0;
263
+ };
264
+ capture();
265
+ port.target.addEventListener("scroll", capture, { passive: true });
266
+ const stop = () => port.target.removeEventListener("scroll", capture);
267
+ return {
268
+ stop,
269
+ restore: () => {
270
+ stop();
271
+ const retained = messages(root), anchor = retained.find((message) => message.dataset.messageId === anchorId);
272
+ if (anchor) port.element.scrollTop += anchor.getBoundingClientRect().top - port.top() - offset;
273
+ else if (anchorId && retained.length) scrollChatToLatest(root);
274
+ }
275
+ };
276
+ }
277
+ function scrollChatToLatest(root) {
278
+ const last = messages(root).at(-1);
279
+ if (!last) return;
280
+ const port = scrollPort(root);
281
+ if (port.element === root) {
282
+ root.scrollTop = root.scrollHeight;
283
+ return;
284
+ }
285
+ port.element.scrollTop += last.getBoundingClientRect().bottom - port.bottom();
286
+ }
287
+ function watchChatNavigationFocus(root, selector, signal) {
288
+ const doc = root.ownerDocument;
289
+ let focused = !!("activeElement" in root ? root.activeElement : doc.activeElement)?.matches(selector);
290
+ const capture = (event) => {
291
+ const element = event.composedPath()[0];
292
+ focused = element instanceof Element && root.contains(element) && element.matches(selector);
293
+ };
294
+ doc.addEventListener("focusin", capture, {
295
+ capture: true,
296
+ ...signal ? { signal } : {}
297
+ });
298
+ const stop = () => doc.removeEventListener("focusin", capture, true);
299
+ return {
300
+ stop,
301
+ restore(fallback) {
302
+ stop();
303
+ if (focused) (root.querySelector(selector) ?? fallback)?.focus({ preventScroll: true });
304
+ }
305
+ };
306
+ }
307
+ const followSlack = 24;
308
+ var ChatFollower = class {
309
+ root;
310
+ signal;
311
+ behind;
312
+ port = null;
313
+ stopListening = null;
314
+ end = null;
315
+ following = true;
316
+ triedLatestAt = null;
317
+ constructor(root, signal, behind = () => {}) {
318
+ this.root = root;
319
+ this.signal = signal;
320
+ this.behind = behind;
321
+ signal.addEventListener("abort", () => this.stopListening?.(), { once: true });
322
+ }
323
+ settle(state) {
324
+ if (this.signal.aborted) return;
325
+ const port = ownScrollContainer(this.root);
326
+ if (port !== this.port) this.listen(port);
327
+ const opened = this.end === null && state.end !== null;
328
+ const shownEnd = this.end;
329
+ const arrived = state.end !== null && this.end !== null && state.end > this.end;
330
+ if (port && (opened || arrived && (this.following || state.lastMine))) scrollForwardTo(port, messages(this.root).at(-1));
331
+ if (state.end !== null) this.end = Math.max(this.end ?? state.end, state.end);
332
+ this.measure();
333
+ const count = state.count;
334
+ if (port && this.following && !opened && state.end !== null && shownEnd !== null && state.end >= shownEnd && count !== null && count > state.end && this.triedLatestAt !== count) {
335
+ this.triedLatestAt = count;
336
+ this.behind();
337
+ }
338
+ }
339
+ listen(port) {
340
+ this.stopListening?.();
341
+ this.stopListening = null;
342
+ this.port = port;
343
+ if (!port) return;
344
+ const measure = () => this.measure();
345
+ port.addEventListener("scroll", measure, { passive: true });
346
+ this.stopListening = () => port.removeEventListener("scroll", measure);
347
+ }
348
+ measure() {
349
+ const last = messages(this.root).at(-1);
350
+ this.following = !this.port || !last || last.getBoundingClientRect().bottom - this.port.getBoundingClientRect().bottom <= followSlack;
351
+ }
352
+ };
353
+ function ownScrollContainer(root) {
354
+ const doc = root.ownerDocument;
355
+ const win = doc.defaultView;
356
+ for (let element = root; element; element = element.parentElement) {
357
+ if (element === doc.body || element === doc.documentElement) return null;
358
+ if (win && /(auto|scroll|overlay)/.test(win.getComputedStyle(element).overflowY)) return element;
359
+ }
360
+ return null;
361
+ }
362
+ function scrollForwardTo(port, message) {
363
+ if (!message) return;
364
+ const beyond = message.getBoundingClientRect().bottom - port.getBoundingClientRect().bottom;
365
+ if (beyond > 0) port.scrollTop += beyond;
366
+ }
367
+ const readEnd = 24;
368
+ var ChatSeenTracker = class {
369
+ options;
370
+ observer = null;
371
+ observed = /* @__PURE__ */ new Map();
372
+ intersecting = /* @__PURE__ */ new Set();
373
+ renderGeneration = 0;
374
+ interactionGeneration = 0;
375
+ failedGeneration = -1;
376
+ inFlight = false;
377
+ frame = 0;
378
+ constructor(options) {
379
+ this.options = options;
380
+ const { root, signal } = options;
381
+ const interaction = () => this.interact();
382
+ for (const type of [
383
+ "pointerdown",
384
+ "wheel",
385
+ "keydown"
386
+ ]) root.addEventListener(type, interaction, {
387
+ signal,
388
+ passive: true
389
+ });
390
+ document.addEventListener("visibilitychange", () => {
391
+ if (document.visibilityState === "visible") this.interact();
392
+ }, { signal });
393
+ const scroll = () => this.recheckSoon();
394
+ for (const target of /* @__PURE__ */ new Set([
395
+ root,
396
+ root.getRootNode(),
397
+ document
398
+ ])) target.addEventListener("scroll", scroll, {
399
+ signal,
400
+ capture: true,
401
+ passive: true
402
+ });
403
+ signal.addEventListener("abort", () => this.disconnect(), { once: true });
404
+ }
405
+ interact() {
406
+ this.interactionGeneration++;
407
+ this.dispatch();
408
+ }
409
+ disconnect() {
410
+ this.renderGeneration++;
411
+ this.observer?.disconnect();
412
+ this.observer = null;
413
+ this.observed.clear();
414
+ this.intersecting.clear();
415
+ if (this.frame) cancelAnimationFrame(this.frame);
416
+ this.frame = 0;
417
+ }
418
+ observe() {
419
+ this.disconnect();
420
+ if (this.options.signal.aborted || !this.options.permitted() || typeof IntersectionObserver === "undefined") return;
421
+ const generation = this.renderGeneration;
422
+ this.observer = new IntersectionObserver((entries) => {
423
+ if (generation !== this.renderGeneration || !this.options.permitted()) return;
424
+ for (const entry of entries) if (entry.isIntersecting) this.intersecting.add(entry.target);
425
+ else this.intersecting.delete(entry.target);
426
+ this.dispatch();
427
+ }, { threshold: [0, 1] });
428
+ for (const element of this.options.root.querySelectorAll("[data-read-message-id]")) {
429
+ const id = element.dataset.readMessageId;
430
+ if (id) {
431
+ this.observed.set(id, element);
432
+ this.observer.observe(element);
433
+ }
434
+ }
435
+ }
436
+ recheckSoon() {
437
+ if (this.frame || this.observed.size === 0) return;
438
+ this.frame = requestAnimationFrame(() => {
439
+ this.frame = 0;
440
+ this.dispatch();
441
+ });
442
+ }
443
+ candidate() {
444
+ const thread = this.options.thread();
445
+ if (this.options.signal.aborted || !this.options.permitted() || document.visibilityState !== "visible" || !thread || thread.userSeen.unreadTeamMessageCount === 0) return null;
446
+ const { lastSeenTeamMessageId, unreadTeamMessageCount } = thread.userSeen;
447
+ const seenIndex = thread.messages.findIndex((message) => message.messageId === lastSeenTeamMessageId);
448
+ let unread = !thread.hasNewerMessages ? unreadTeamMessageCount : seenIndex === -1 ? 0 : Infinity;
449
+ for (let index = thread.messages.length - 1; index > seenIndex && unread > 0; index--) {
450
+ const message = thread.messages[index];
451
+ if (!message || message.authorType !== "team") continue;
452
+ unread--;
453
+ const element = this.observed.get(message.messageId);
454
+ if (element && this.intersecting.has(element) && this.options.root.contains(element) && endOnScreen(element)) return message.messageId;
455
+ }
456
+ return null;
457
+ }
458
+ dispatch() {
459
+ if (this.inFlight || this.failedGeneration === this.interactionGeneration) return;
460
+ const messageId = this.candidate();
461
+ const threadId = this.options.thread()?.threadId;
462
+ if (!messageId || !threadId) return;
463
+ this.inFlight = true;
464
+ this.options.markSeen(threadId, messageId).then(() => {
465
+ if (this.candidate() === messageId) this.failedGeneration = this.interactionGeneration;
466
+ }).catch(() => {
467
+ this.failedGeneration = this.interactionGeneration;
468
+ }).finally(() => {
469
+ this.inFlight = false;
470
+ this.dispatch();
471
+ });
472
+ }
473
+ };
474
+ function endOnScreen(element) {
475
+ if (!element.isConnected) return false;
476
+ const rect = element.getBoundingClientRect();
477
+ if (rect.width <= 0 || rect.height <= 0) return false;
478
+ let left = 0, top = 0, right = window.innerWidth, bottom = window.innerHeight;
479
+ let current = element;
480
+ while (current) {
481
+ const style = getComputedStyle(current);
482
+ if (current.hasAttribute("hidden") || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number.parseFloat(style.opacity) === 0) return false;
483
+ if (current !== element) {
484
+ const clip = current.getBoundingClientRect();
485
+ if (/hidden|clip|scroll|auto/.test(style.overflowX)) {
486
+ left = Math.max(left, clip.left);
487
+ right = Math.min(right, clip.right);
488
+ }
489
+ if (/hidden|clip|scroll|auto/.test(style.overflowY)) {
490
+ top = Math.max(top, clip.top);
491
+ bottom = Math.min(bottom, clip.bottom);
492
+ }
493
+ }
494
+ const tree = current.getRootNode();
495
+ current = current.parentElement ?? (tree instanceof ShadowRoot ? tree.host : null);
496
+ }
497
+ const endTop = rect.bottom - Math.min(readEnd, rect.height);
498
+ return rect.left >= left && rect.right <= right && endTop >= top && rect.bottom <= bottom;
499
+ }
500
+ function saveFile(file, fileName, container = document.body) {
501
+ const href = URL.createObjectURL(file);
502
+ const link = document.createElement("a");
503
+ link.href = href;
504
+ link.download = fileName;
505
+ link.target = "_self";
506
+ container.append(link);
507
+ link.click();
508
+ link.remove();
509
+ setTimeout(() => URL.revokeObjectURL(href), 6e4);
510
+ }
511
+ function attachmentFailureCopy(code) {
512
+ return `couldn't attach. ${code === "invalid_file_name" ? "that file name can't be used." : code === "unsupported_type" ? "that file type isn't supported." : code === "too_large" ? `that file is larger than ${chatAttachmentMaxByteSize / 1024 / 1024} MB.` : code === "classification_failed" || code === "type_mismatch" || code === "read_failed" || code === "size_mismatch" ? "that file couldn't be read." : "try again."}`;
513
+ }
514
+ function sameAttempt(kept, text, attachmentIds) {
515
+ return kept.text === text && kept.attachmentIds.length === attachmentIds.length && kept.attachmentIds.every((id, index) => id === attachmentIds[index]);
516
+ }
517
+ function ChatComposer({ threadId, attachments, children, ...rest }) {
518
+ const { actions } = useChatThread(threadId);
519
+ const [draft, setDraft] = useState("");
520
+ const [sending, setSending] = useState(false);
521
+ const [error, setError] = useState(null);
522
+ const attempts = useRef(/* @__PURE__ */ new Map());
523
+ async function send() {
524
+ const text = draft.trim();
525
+ if (!text || sending || (attachments?.pending ?? false)) return;
526
+ setSending(true);
527
+ setError(null);
528
+ try {
529
+ const references = attachments?.references() ?? [];
530
+ const attachmentIds = references.map((reference) => reference.attachmentId);
531
+ const kept = attempts.current.get(threadId);
532
+ const key = kept && sameAttempt(kept, text, attachmentIds) ? kept.key : uuid();
533
+ attempts.current.set(threadId, {
534
+ key,
535
+ text,
536
+ attachmentIds
537
+ });
538
+ await actions.sendMessage(references.length > 0 ? {
539
+ text,
540
+ attachments: references
541
+ } : { text }, { idempotencyKey: key });
542
+ attempts.current.delete(threadId);
543
+ setDraft("");
544
+ attachments?.clear();
545
+ } catch (caught) {
546
+ setError(asDropByError(caught));
547
+ } finally {
548
+ setSending(false);
549
+ }
550
+ }
551
+ function handleSubmit(event) {
552
+ event.preventDefault();
553
+ send();
554
+ }
555
+ if (children) return /* @__PURE__ */ jsx("form", {
556
+ onSubmit: handleSubmit,
557
+ ...rest,
558
+ children: children({
559
+ draft,
560
+ setDraft,
561
+ send,
562
+ sending,
563
+ error,
564
+ attachments
565
+ })
566
+ });
567
+ const sendDisabled = sending || draft.trim().length === 0 || (attachments?.pending ?? false);
568
+ return /* @__PURE__ */ jsxs("form", {
569
+ onSubmit: handleSubmit,
570
+ ...rest,
571
+ children: [
572
+ /* @__PURE__ */ jsx("textarea", {
573
+ "aria-label": "message",
574
+ onChange: (event) => setDraft(event.target.value),
575
+ value: draft
576
+ }),
577
+ attachments ? /* @__PURE__ */ jsxs("div", {
578
+ "data-attachments": "",
579
+ children: [
580
+ /* @__PURE__ */ jsx("input", {
581
+ type: "file",
582
+ multiple: true,
583
+ accept: attachments.accept,
584
+ "aria-label": "attach files",
585
+ onChange: (event) => {
586
+ const { files } = event.target;
587
+ if (files && files.length > 0) attachments.add(files);
588
+ event.target.value = "";
589
+ }
590
+ }),
591
+ attachments.notice ? /* @__PURE__ */ jsx("p", {
592
+ role: "alert",
593
+ children: attachments.notice
594
+ }) : null,
595
+ attachments.drafts.length > 0 ? /* @__PURE__ */ jsx("ul", {
596
+ "aria-label": "attachments",
597
+ children: attachments.drafts.map((item) => /* @__PURE__ */ jsxs("li", {
598
+ "data-status": item.status,
599
+ "data-attachment-kind": item.kind,
600
+ children: [
601
+ item.previewUrl ? /* @__PURE__ */ jsx("img", {
602
+ src: item.previewUrl,
603
+ alt: "",
604
+ width: 48,
605
+ height: 48
606
+ }) : null,
607
+ /* @__PURE__ */ jsx("span", { children: item.fileName }),
608
+ item.error ? /* @__PURE__ */ jsx("span", {
609
+ role: "alert",
610
+ children: attachmentFailureCopy(item.error.code)
611
+ }) : null,
612
+ /* @__PURE__ */ jsx("button", {
613
+ type: "button",
614
+ "aria-label": `remove ${item.fileName}`,
615
+ onClick: () => attachments.remove(item.localId),
616
+ children: "remove"
617
+ })
618
+ ]
619
+ }, item.localId))
620
+ }) : null
621
+ ]
622
+ }) : null,
623
+ error ? /* @__PURE__ */ jsx("p", {
624
+ role: "alert",
625
+ children: "couldn't send. your message is still here."
626
+ }) : null,
627
+ /* @__PURE__ */ jsx("button", {
628
+ type: "submit",
629
+ "aria-busy": sending || void 0,
630
+ "data-busy": sending ? "" : void 0,
631
+ disabled: sendDisabled,
632
+ children: "send"
633
+ })
634
+ ]
635
+ });
636
+ }
637
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
638
+ function useChatNavigation(id, latest) {
639
+ const root = useRef(null);
640
+ const generation = useRef(0);
641
+ const [, settled] = useState(0);
642
+ const pending = useRef(null);
643
+ useEffect(() => () => {
644
+ generation.current++;
645
+ pending.current?.position.stop();
646
+ pending.current?.focus.stop();
647
+ pending.current = null;
648
+ }, [id]);
649
+ useIsomorphicLayoutEffect(() => {
650
+ const action = pending.current, element = root.current;
651
+ if (!action?.finished || !element) return;
652
+ pending.current = null;
653
+ if (action.kind === "latest" && latest?.status === "success") {
654
+ action.position.stop();
655
+ scrollChatToLatest(element);
656
+ } else action.position.restore();
657
+ action.focus.restore(element);
658
+ });
659
+ async function navigate(kind, run) {
660
+ const element = root.current;
661
+ if (!element) return;
662
+ const ticket = ++generation.current;
663
+ pending.current?.position.stop();
664
+ pending.current?.focus.stop();
665
+ pending.current = {
666
+ kind,
667
+ position: watchChatScrollPosition(element),
668
+ focus: watchChatNavigationFocus(element, `[data-history-action="${kind}"]`),
669
+ finished: false
670
+ };
671
+ try {
672
+ await run();
673
+ } finally {
674
+ if (ticket === generation.current && pending.current) {
675
+ pending.current.finished = true;
676
+ settled((value) => value + 1);
677
+ }
678
+ }
679
+ }
680
+ return {
681
+ root,
682
+ navigate
683
+ };
684
+ }
685
+ function mayAcknowledge(core) {
686
+ const capabilities = core.capabilities.getSnapshot();
687
+ return capabilities.chat.canRead && !capabilities.actionDisabled("chat.thread:seen");
688
+ }
689
+ function useChatSeen(core, root, id, enabled) {
690
+ const tracker = useRef(null);
691
+ useEffect(() => {
692
+ const element = root.current;
693
+ if (!enabled || !element) return;
694
+ const lifetime = new AbortController();
695
+ const current = new ChatSeenTracker({
696
+ root: element,
697
+ signal: lifetime.signal,
698
+ thread: () => core.chat.getSnapshot().threadsById[id]?.data ?? null,
699
+ permitted: () => mayAcknowledge(core),
700
+ markSeen: (threadId, messageId) => core.chat.markThreadSeen(threadId, { seenThroughMessageId: messageId })
701
+ });
702
+ tracker.current = current;
703
+ let permitted = mayAcknowledge(core);
704
+ const stopCapabilities = core.capabilities.subscribe(() => {
705
+ const now = mayAcknowledge(core);
706
+ if (now && !permitted) {
707
+ current.observe();
708
+ current.interact();
709
+ }
710
+ permitted = now;
711
+ });
712
+ const live = () => ["open", "polling"].includes(core.connection.getSnapshot().status);
713
+ let connected = live();
714
+ const stopConnection = core.connection.subscribe(() => {
715
+ const now = live();
716
+ if (now && !connected) current.interact();
717
+ connected = now;
718
+ });
719
+ current.observe();
720
+ return () => {
721
+ lifetime.abort();
722
+ stopCapabilities();
723
+ stopConnection();
724
+ tracker.current = null;
725
+ };
726
+ }, [
727
+ core,
728
+ root,
729
+ id,
730
+ enabled
731
+ ]);
732
+ useEffect(() => {
733
+ tracker.current?.observe();
734
+ });
735
+ }
736
+ function useChatFollow(root, id, thread, enabled, loadLatest) {
737
+ const follower = useRef(null);
738
+ const latest = useRef(loadLatest);
739
+ latest.current = loadLatest;
740
+ useIsomorphicLayoutEffect(() => {
741
+ const element = root.current;
742
+ if (!enabled || !element) return;
743
+ const lifetime = new AbortController();
744
+ follower.current = new ChatFollower(element, lifetime.signal, () => latest.current());
745
+ return () => {
746
+ lifetime.abort();
747
+ follower.current = null;
748
+ };
749
+ }, [
750
+ root,
751
+ id,
752
+ enabled
753
+ ]);
754
+ const end = thread?.messages.at(-1)?.sequence ?? null;
755
+ const lastMine = thread?.messages.at(-1)?.authorType === "user";
756
+ const count = thread?.messageCount ?? null;
757
+ useIsomorphicLayoutEffect(() => {
758
+ follower.current?.settle({
759
+ end,
760
+ lastMine,
761
+ count
762
+ });
763
+ }, [
764
+ id,
765
+ end,
766
+ lastMine,
767
+ count
768
+ ]);
769
+ }
770
+ function ChatThread({ id, manualLoad, acknowledge = true, children, ...rest }) {
771
+ const { data, actions, olderMessages, latestMessages } = useChatThread(id);
772
+ const core = useDropBy();
773
+ const navigation = useChatNavigation(id, latestMessages);
774
+ const status = data?.status ?? "idle";
775
+ useChatSeen(core, navigation.root, id, acknowledge && !children);
776
+ useChatFollow(navigation.root, id, data?.data, !children, () => {
777
+ actions.loadLatestMessages();
778
+ });
779
+ useEffect(() => {
780
+ if (!manualLoad && status === "idle") actions.open();
781
+ }, [
782
+ manualLoad,
783
+ status,
784
+ actions
785
+ ]);
786
+ if (children) return children({
787
+ thread: data,
788
+ actions,
789
+ olderMessages,
790
+ latestMessages
791
+ });
792
+ const thread = data?.data;
793
+ return /* @__PURE__ */ jsxs("div", {
794
+ "data-status": status,
795
+ tabIndex: -1,
796
+ ...rest,
797
+ ref: navigation.root,
798
+ children: [
799
+ thread?.hasOlderMessages ? /* @__PURE__ */ jsx("button", {
800
+ type: "button",
801
+ "data-history-action": "older",
802
+ disabled: olderMessages?.status === "loading" || latestMessages?.status === "loading",
803
+ onClick: () => void navigation.navigate("older", () => actions.loadOlderMessages()),
804
+ children: olderMessages?.status === "loading" ? "loading older messages" : "load older messages"
805
+ }) : null,
806
+ status === "error" && !thread ? /* @__PURE__ */ jsxs("p", {
807
+ role: "alert",
808
+ children: [
809
+ "couldn't open this conversation.",
810
+ " ",
811
+ /* @__PURE__ */ jsx("button", {
812
+ type: "button",
813
+ onClick: () => void actions.open(),
814
+ children: "try again"
815
+ })
816
+ ]
817
+ }) : null,
818
+ olderMessages?.error ? /* @__PURE__ */ jsx("p", {
819
+ role: "alert",
820
+ children: "couldn't load older messages. try again."
821
+ }) : null,
822
+ /* @__PURE__ */ jsx("ul", {
823
+ "aria-live": "polite",
824
+ children: thread?.messages.map((message) => /* @__PURE__ */ jsxs("li", {
825
+ "data-message-id": message.messageId,
826
+ "data-read-message-id": message.authorType === "team" ? message.messageId : void 0,
827
+ "data-author": message.authorType,
828
+ children: [message.text, message.attachments.length > 0 ? /* @__PURE__ */ jsx("ul", { children: message.attachments.map((attachment) => /* @__PURE__ */ jsx(AttachmentDownload, {
829
+ attachment,
830
+ fetchFile: (attachmentId) => fetchChatAttachmentFile(core, attachmentId)
831
+ }, attachment.attachmentId)) }) : null]
832
+ }, message.messageId))
833
+ }),
834
+ thread && (thread.hasNewerMessages || latestMessages?.status === "loading" || latestMessages?.error) ? /* @__PURE__ */ jsx("button", {
835
+ type: "button",
836
+ "data-history-action": "latest",
837
+ disabled: latestMessages?.status === "loading",
838
+ onClick: () => void navigation.navigate("latest", () => actions.loadLatestMessages()),
839
+ children: latestMessages?.status === "loading" ? "loading latest messages" : "back to latest"
840
+ }) : null,
841
+ latestMessages?.error ? /* @__PURE__ */ jsx("p", {
842
+ role: "alert",
843
+ children: "couldn't load latest messages. try again."
844
+ }) : null,
845
+ /* @__PURE__ */ jsx(ChatComposer, { threadId: id })
846
+ ]
847
+ });
848
+ }
849
+ function AttachmentDownload({ attachment, fetchFile }) {
850
+ const [downloading, setDownloading] = useState(false);
851
+ const [error, setError] = useState(null);
852
+ async function download() {
853
+ setDownloading(true);
854
+ setError(null);
855
+ try {
856
+ saveFile(await fetchFile(attachment.attachmentId), attachment.fileName);
857
+ } catch (caught) {
858
+ setError(caught instanceof Error ? caught : new Error(String(caught)));
859
+ } finally {
860
+ setDownloading(false);
861
+ }
862
+ }
863
+ return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("button", {
864
+ type: "button",
865
+ "aria-label": `download ${attachment.fileName}`,
866
+ "aria-busy": downloading || void 0,
867
+ "data-attachment-kind": attachment.kind,
868
+ "data-busy": downloading ? "" : void 0,
869
+ "data-error": error ? "" : void 0,
870
+ disabled: downloading,
871
+ onClick: () => void download(),
872
+ children: attachment.fileName
873
+ }), error ? /* @__PURE__ */ jsxs("span", {
874
+ role: "alert",
875
+ children: [
876
+ "couldn't download ",
877
+ attachment.fileName,
878
+ ". try again."
879
+ ]
880
+ }) : null] });
881
+ }
882
+ function ChatThreadList({ manualLoad, onSelect, selectedId, children, ...rest }) {
883
+ const { data, actions } = useChatThreads();
884
+ useEffect(() => {
885
+ if (!manualLoad && data.status === "idle") actions.listThreads();
886
+ }, [
887
+ manualLoad,
888
+ data.status,
889
+ actions
890
+ ]);
891
+ if (children) return children({
892
+ threads: data,
893
+ actions
894
+ });
895
+ const empty = data.status === "success" && data.items.length === 0;
896
+ return /* @__PURE__ */ jsxs("ul", {
897
+ "aria-live": "polite",
898
+ "data-status": data.status,
899
+ "data-empty": empty ? "" : void 0,
900
+ ...rest,
901
+ children: [
902
+ /* @__PURE__ */ jsxs("li", { children: [
903
+ /* @__PURE__ */ jsx("button", {
904
+ type: "button",
905
+ disabled: data.status === "loading",
906
+ onClick: () => void actions.listThreads({ filter: data.filter }),
907
+ children: "refresh"
908
+ }),
909
+ /* @__PURE__ */ jsx("button", {
910
+ type: "button",
911
+ "aria-pressed": data.filter === "unread",
912
+ onClick: () => void actions.listThreads({ filter: data.filter === "unread" ? "all" : "unread" }),
913
+ children: "unread"
914
+ }),
915
+ data.status === "error" ? /* @__PURE__ */ jsx("span", {
916
+ role: "alert",
917
+ children: "couldn't load conversations. try again."
918
+ }) : null
919
+ ] }),
920
+ data.items.map((thread) => {
921
+ const unread = thread.userSeen.unreadTeamMessageCount;
922
+ const summary = /* @__PURE__ */ jsxs(Fragment, { children: [thread.lastMessagePreview || "conversation", unread > 0 && /* @__PURE__ */ jsxs("span", { children: [
923
+ " · ",
924
+ unread,
925
+ " unread replies"
926
+ ] })] });
927
+ return /* @__PURE__ */ jsx("li", {
928
+ "data-status": thread.status,
929
+ "data-unread": unread > 0 ? "" : void 0,
930
+ children: onSelect ? /* @__PURE__ */ jsx("button", {
931
+ type: "button",
932
+ "aria-current": thread.threadId === selectedId ? "true" : void 0,
933
+ onClick: () => onSelect(thread.threadId),
934
+ children: summary
935
+ }) : summary
936
+ }, thread.threadId);
937
+ }),
938
+ data.hasMore || data.status === "success" && data.error ? /* @__PURE__ */ jsxs("li", {
939
+ "data-load-more": "",
940
+ children: [data.status === "success" && data.error ? /* @__PURE__ */ jsx("span", {
941
+ role: "alert",
942
+ children: "couldn't load more conversations. try again."
943
+ }) : null, /* @__PURE__ */ jsx("button", {
944
+ type: "button",
945
+ disabled: data.loadingMore,
946
+ onClick: () => void actions.loadMoreThreads(),
947
+ children: data.loadingMore ? "loading more conversations" : "show more conversations"
948
+ })]
949
+ }) : null
950
+ ]
951
+ });
952
+ }
953
+ function Feature({ name, equals, fallback = null, children }) {
954
+ const value = useFeatureFlag(name);
955
+ return (equals === void 0 ? Boolean(value) : value === equals) ? children : fallback;
956
+ }
957
+ function useIdeaVote(idea) {
958
+ const { actions } = useIdeas();
959
+ const [voting, setVoting] = useState(false);
960
+ const [error, setError] = useState(null);
961
+ const voted = idea.viewerHasVoted;
962
+ async function run(action) {
963
+ setVoting(true);
964
+ setError(null);
965
+ try {
966
+ await action();
967
+ } catch (caught) {
968
+ setError(asDropByError(caught));
969
+ } finally {
970
+ setVoting(false);
971
+ }
972
+ }
973
+ return {
974
+ idea,
975
+ voted,
976
+ votable: isVotableIdea(idea),
977
+ voting,
978
+ error,
979
+ vote: () => run(() => actions.vote(idea.ideaId)),
980
+ unvote: () => run(() => actions.unvote(idea.ideaId)),
981
+ toggle: () => run(() => voted ? actions.unvote(idea.ideaId) : actions.vote(idea.ideaId))
982
+ };
983
+ }
984
+ function IdeaVoteButton({ idea, children, onClick, disabled, ...rest }) {
985
+ const state = useIdeaVote(idea);
986
+ function handleClick(event) {
987
+ onClick?.(event);
988
+ if (event.defaultPrevented) return;
989
+ state.toggle();
990
+ }
991
+ const content = typeof children === "function" ? children(state) : children ?? idea.voteCount;
992
+ return /* @__PURE__ */ jsx("button", {
993
+ type: "button",
994
+ "aria-pressed": state.voted,
995
+ "aria-busy": state.voting || void 0,
996
+ "data-voted": state.voted ? "" : void 0,
997
+ "data-busy": state.voting ? "" : void 0,
998
+ disabled: disabled ?? (state.voting || !state.votable),
999
+ onClick: handleClick,
1000
+ ...rest,
1001
+ children: content
1002
+ });
1003
+ }
1004
+ function Idea({ idea, children, ...rest }) {
1005
+ const voted = idea.viewerHasVoted ? "" : void 0;
1006
+ if (typeof children === "function") return /* @__PURE__ */ jsx("div", {
1007
+ "data-voted": voted,
1008
+ ...rest,
1009
+ children: children({ idea })
1010
+ });
1011
+ return /* @__PURE__ */ jsxs("div", {
1012
+ "data-voted": voted,
1013
+ ...rest,
1014
+ children: [
1015
+ /* @__PURE__ */ jsx(IdeaVoteButton, { idea }),
1016
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("strong", { children: idea.title }), idea.description ? /* @__PURE__ */ jsx("p", { children: idea.description }) : null] }),
1017
+ children
1018
+ ]
1019
+ });
1020
+ }
1021
+ function IdeaList({ view, manualLoad, children, ...rest }) {
1022
+ const { data, actions } = useIdeas();
1023
+ useEffect(() => {
1024
+ if (!manualLoad && data.status === "idle") actions.list({ view });
1025
+ }, [
1026
+ manualLoad,
1027
+ view,
1028
+ data.status,
1029
+ actions
1030
+ ]);
1031
+ if (typeof children === "function") return children({
1032
+ ideas: data,
1033
+ actions
1034
+ });
1035
+ const empty = data.status === "success" && data.items.length === 0;
1036
+ return /* @__PURE__ */ jsxs("ul", {
1037
+ "aria-live": "polite",
1038
+ "data-status": data.status,
1039
+ "data-empty": empty ? "" : void 0,
1040
+ ...rest,
1041
+ children: [data.items.map((idea) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Idea, { idea }) }, idea.ideaId)), data.hasMore ? /* @__PURE__ */ jsx("li", {
1042
+ "data-load-more": "",
1043
+ children: /* @__PURE__ */ jsx("button", {
1044
+ disabled: data.loadingMore,
1045
+ onClick: () => void actions.loadMore(),
1046
+ type: "button",
1047
+ children: data.loadingMore ? "loading more ideas" : "show more ideas"
1048
+ })
1049
+ }) : null]
1050
+ });
1051
+ }
1052
+ function PendingAsks({ fieldsPerStep, manualLoad, children, ...rest }) {
1053
+ const { data, actions } = usePendingAsks();
1054
+ useEffect(() => {
1055
+ if (!manualLoad && data.status === "idle") actions.listPending();
1056
+ }, [
1057
+ manualLoad,
1058
+ data.status,
1059
+ actions
1060
+ ]);
1061
+ if (children) return children({
1062
+ asks: data,
1063
+ actions
1064
+ });
1065
+ const empty = data.status === "success" && data.items.length === 0;
1066
+ return /* @__PURE__ */ jsxs("ul", {
1067
+ "aria-live": "polite",
1068
+ "data-status": data.status,
1069
+ "data-empty": empty ? "" : void 0,
1070
+ ...rest,
1071
+ children: [data.items.map((ask) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Ask, {
1072
+ ask,
1073
+ ...fieldsPerStep !== void 0 ? { fieldsPerStep } : {}
1074
+ }) }, ask.askId)), data.status === "error" ? /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
1075
+ role: "status",
1076
+ children: "couldn’t load asks."
1077
+ }), /* @__PURE__ */ jsx("button", {
1078
+ type: "button",
1079
+ onClick: () => void actions.listPending(),
1080
+ children: "retry"
1081
+ })] }) : data.hasMore || data.error ? /* @__PURE__ */ jsxs("li", {
1082
+ "data-load-more": "",
1083
+ children: [data.error ? /* @__PURE__ */ jsx("span", {
1084
+ role: "status",
1085
+ children: "couldn’t load more asks."
1086
+ }) : null, /* @__PURE__ */ jsx("button", {
1087
+ type: "button",
1088
+ disabled: data.loadingMore,
1089
+ onClick: () => void actions.loadMorePending(),
1090
+ children: data.loadingMore ? "loading more asks" : "show more asks"
1091
+ })]
1092
+ }) : null]
1093
+ });
1094
+ }
1095
+ export { Ask, AskForm, ChatComposer, ChatThread, ChatThreadList, Feature, Idea, IdeaList, IdeaVoteButton, PendingAsks };