@adatechnology/conversations-ui 0.4.2 → 0.5.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.
Files changed (93) hide show
  1. package/README.md +169 -0
  2. package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
  3. package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
  4. package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
  5. package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
  6. package/dist/chunk-MRVFFJEH.js +64 -0
  7. package/dist/chunk-RJMSM3YA.js +249 -0
  8. package/dist/chunk-WSFGNR5R.js +55 -0
  9. package/dist/flows/index.js +5 -3
  10. package/dist/index.d.ts +9 -3
  11. package/dist/index.js +783 -807
  12. package/dist/participant/index.d.ts +162 -0
  13. package/dist/participant/index.js +1448 -0
  14. package/dist/preview/index.d.ts +4 -2
  15. package/dist/preview/index.js +4 -2
  16. package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
  17. package/dist/styles.css +591 -0
  18. package/package.json +8 -4
  19. package/src/ChannelIcon.tsx +2 -0
  20. package/src/MessageBubble.test.tsx +2 -2
  21. package/src/MessageText.test.tsx +59 -0
  22. package/src/MessageText.tsx +20 -12
  23. package/src/MessageTextCopiedBadge.tsx +7 -0
  24. package/src/StatusTicks.test.tsx +30 -0
  25. package/src/StatusTicks.tsx +24 -9
  26. package/src/buildOutput.test.ts +41 -0
  27. package/src/channelCapability.test.ts +17 -0
  28. package/src/conversationChannel.test.ts +30 -1
  29. package/src/conversationChannel.ts +19 -1
  30. package/src/participant/ParticipantAttachmentItem.tsx +77 -0
  31. package/src/participant/ParticipantComposer.test.tsx +97 -0
  32. package/src/participant/ParticipantComposer.tsx +167 -0
  33. package/src/participant/ParticipantConversations.test.tsx +90 -0
  34. package/src/participant/ParticipantConversations.tsx +42 -0
  35. package/src/participant/ParticipantConversationsScreen.tsx +67 -0
  36. package/src/participant/ParticipantInbox.test.tsx +147 -0
  37. package/src/participant/ParticipantInbox.tsx +168 -0
  38. package/src/participant/ParticipantLoadState.tsx +27 -0
  39. package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
  40. package/src/participant/ParticipantMessageBubble.tsx +185 -0
  41. package/src/participant/ParticipantThread.test.tsx +132 -0
  42. package/src/participant/ParticipantThread.tsx +174 -0
  43. package/src/participant/ParticipantThreadScreen.tsx +135 -0
  44. package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
  45. package/src/participant/bindAttachmentUrlResolver.ts +7 -0
  46. package/src/participant/index.ts +25 -0
  47. package/src/participant/participant.types.ts +43 -0
  48. package/src/participant/participantApi.types.ts +59 -0
  49. package/src/participant/participantAttachmentUrl.test.ts +32 -0
  50. package/src/participant/participantAttachmentUrl.ts +10 -0
  51. package/src/participant/participantBubbleActions.test.ts +44 -0
  52. package/src/participant/participantBubbleActions.ts +40 -0
  53. package/src/participant/participantDrafts.test.ts +65 -0
  54. package/src/participant/participantDrafts.ts +50 -0
  55. package/src/participant/participantFileValidation.test.ts +86 -0
  56. package/src/participant/participantFileValidation.ts +72 -0
  57. package/src/participant/participantFixtures.test-helper.ts +41 -0
  58. package/src/participant/participantGrouping.test.ts +138 -0
  59. package/src/participant/participantGrouping.ts +77 -0
  60. package/src/participant/participantInboxState.test.ts +69 -0
  61. package/src/participant/participantInboxState.ts +74 -0
  62. package/src/participant/participantLabels.ts +86 -0
  63. package/src/participant/participantLoadView.test.ts +23 -0
  64. package/src/participant/participantLoadView.ts +21 -0
  65. package/src/participant/participantMessages.test.ts +128 -0
  66. package/src/participant/participantMessages.ts +73 -0
  67. package/src/participant/participantNewMessages.test.ts +54 -0
  68. package/src/participant/participantNewMessages.ts +29 -0
  69. package/src/participant/participantQuickReply.test.ts +23 -0
  70. package/src/participant/participantQuickReply.ts +6 -0
  71. package/src/participant/participantScroll.test.ts +69 -0
  72. package/src/participant/participantScroll.ts +43 -0
  73. package/src/participant/participantSendController.test.ts +201 -0
  74. package/src/participant/participantSendController.ts +134 -0
  75. package/src/participant/participantSendStates.ts +31 -0
  76. package/src/participant/participantTheme.ts +20 -0
  77. package/src/participant/participantThreadState.test.ts +75 -0
  78. package/src/participant/participantThreadState.ts +59 -0
  79. package/src/participant/shouldMarkRead.test.ts +24 -0
  80. package/src/participant/shouldMarkRead.ts +16 -0
  81. package/src/participant/useDocumentVisibility.ts +18 -0
  82. package/src/participant/useParticipantConversationView.ts +60 -0
  83. package/src/participant/useParticipantDraft.ts +45 -0
  84. package/src/participant/useParticipantInbox.ts +57 -0
  85. package/src/participant/useParticipantMarkRead.ts +26 -0
  86. package/src/participant/useParticipantRevalidation.ts +36 -0
  87. package/src/participant/useParticipantSend.ts +84 -0
  88. package/src/participant/useParticipantThread.ts +116 -0
  89. package/src/participant/useParticipantThreadScroll.ts +26 -0
  90. package/src/participant/useStickToBottom.ts +77 -0
  91. package/src/preview/ConversationSimulatorPanel.tsx +8 -0
  92. package/src/styles.css +160 -0
  93. package/src/types.ts +3 -1
@@ -0,0 +1,1448 @@
1
+ import {
2
+ MessageText
3
+ } from "../chunk-WSFGNR5R.js";
4
+ import {
5
+ StatusTicks,
6
+ channelCapabilityFor,
7
+ formatFileSize,
8
+ formatTimestamp,
9
+ isSameDay
10
+ } from "../chunk-RJMSM3YA.js";
11
+ import "../chunk-CBYEHPX6.js";
12
+
13
+ // src/participant/ParticipantConversationsScreen.tsx
14
+ import { useMemo as useMemo4, useReducer as useReducer2, useRef as useRef7, useState as useState7 } from "react";
15
+
16
+ // src/participant/participantLabels.ts
17
+ var DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS = {
18
+ inboxTitle: "Conversations",
19
+ sectionAwaiting: "Waiting for your reply",
20
+ sectionClosed: "Closed",
21
+ sectionOther: "Other",
22
+ filterAll: "All",
23
+ filtersGroup: "Filter by subject",
24
+ emptyInbox: "No conversations yet",
25
+ unreadCount: "{count} unread",
26
+ back: "Back",
27
+ openSubject: "Open subject",
28
+ me: "Me",
29
+ newMessages: "New messages",
30
+ loadOlder: "Load older messages",
31
+ closedNotice: "This conversation is closed",
32
+ messageInputLabel: "Message",
33
+ messageInputPlaceholder: "Write a message\u2026",
34
+ send: "Send",
35
+ attach: "Attach",
36
+ attachmentsList: "Selected attachments",
37
+ removeAttachment: "Remove attachment",
38
+ attachmentTooLarge: "Attachment too large",
39
+ attachmentTypeNotAccepted: "File type not accepted",
40
+ quickRepliesGroup: "Quick replies",
41
+ downloadAttachment: "Download",
42
+ statusSending: "Sending",
43
+ statusQueued: "Queued \u2014 sends when back online",
44
+ statusSent: "Sent",
45
+ statusDelivered: "Delivered",
46
+ statusRead: "Read",
47
+ statusFailed: "Failed \u2014 tap to retry",
48
+ statusFailedShort: "Failed",
49
+ retry: "Retry",
50
+ discard: "Discard",
51
+ edit: "Edit",
52
+ notFound: "Conversation not found",
53
+ loading: "Loading\u2026",
54
+ loadError: "Could not load. Check your connection and try again.",
55
+ loadMore: "Load more"
56
+ };
57
+ function formatParticipantLabel(template, count) {
58
+ return template.replace("{count}", String(count));
59
+ }
60
+
61
+ // src/participant/participantLoadView.ts
62
+ function resolveLoadView({ status, hasItems }) {
63
+ return {
64
+ isLoading: (status === "idle" || status === "loading") && !hasItems,
65
+ hasError: status === "error",
66
+ isEmpty: status === "ready" && !hasItems
67
+ };
68
+ }
69
+
70
+ // src/participant/ParticipantLoadState.tsx
71
+ import { jsx, jsxs } from "react/jsx-runtime";
72
+ function ParticipantLoadError({ labels, onRetry }) {
73
+ return /* @__PURE__ */ jsxs("div", { className: "cv-p-error", role: "alert", children: [
74
+ /* @__PURE__ */ jsx("p", { className: "cv-p-error__message", children: labels.loadError }),
75
+ /* @__PURE__ */ jsx("button", { type: "button", className: "cv-p-button", onClick: onRetry, children: labels.retry })
76
+ ] });
77
+ }
78
+ function ParticipantLoading({ labels }) {
79
+ return /* @__PURE__ */ jsxs("div", { className: "cv-p-loading", children: [
80
+ /* @__PURE__ */ jsx("span", { className: "cv-p-skeleton", "aria-hidden": "true" }),
81
+ /* @__PURE__ */ jsx("span", { className: "cv-p-skeleton", "aria-hidden": "true" }),
82
+ /* @__PURE__ */ jsx("span", { className: "cv-p-sr-only", children: labels.loading })
83
+ ] });
84
+ }
85
+
86
+ // src/participant/ParticipantInbox.tsx
87
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
88
+ var ALL_FILTER = "all";
89
+ function formatRowTime(iso, locale) {
90
+ const date = new Date(iso);
91
+ if (isSameDay(date, /* @__PURE__ */ new Date())) return formatTimestamp(iso);
92
+ return date.toLocaleDateString(locale, { day: "2-digit", month: "2-digit" });
93
+ }
94
+ function sectionTitle(section, props) {
95
+ if (section.key === "awaiting") return props.labels.sectionAwaiting;
96
+ if (section.key === "closed") return props.labels.sectionClosed;
97
+ if (section.key === "other") return props.labels.sectionOther;
98
+ return props.subjectGroups.find((group) => group.subjectType === section.key)?.label ?? section.key;
99
+ }
100
+ function Filters({ filters, active, labels, onChange }) {
101
+ return /* @__PURE__ */ jsx2("div", { className: "cv-p-filters", role: "group", "aria-label": labels.filtersGroup, children: filters.map((filter) => /* @__PURE__ */ jsxs2(
102
+ "button",
103
+ {
104
+ type: "button",
105
+ className: "cv-p-chip",
106
+ "aria-pressed": filter.subjectType === active,
107
+ onClick: () => onChange(filter.subjectType),
108
+ children: [
109
+ filter.subjectType === ALL_FILTER ? labels.filterAll : filter.label,
110
+ filter.unreadCount > 0 ? /* @__PURE__ */ jsx2("b", { className: "cv-p-chip__count", children: filter.unreadCount }) : null
111
+ ]
112
+ },
113
+ filter.subjectType
114
+ )) });
115
+ }
116
+ function Row({ conversation, group, labels, locale, onSelect }) {
117
+ const kind = group?.label ?? conversation.subjectType;
118
+ const className = conversation.awaitingParticipant ? "cv-p-row cv-p-row--awaiting" : "cv-p-row";
119
+ const { subjectType, subjectId } = conversation;
120
+ return /* @__PURE__ */ jsxs2("button", { type: "button", className, onClick: () => onSelect({ subjectType, subjectId }), children: [
121
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-row__icon", "aria-hidden": "true", children: group?.icon ?? kind.slice(0, 2) }),
122
+ /* @__PURE__ */ jsxs2("span", { className: "cv-p-row__body", children: [
123
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-row__kind", children: kind }),
124
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-row__title", children: conversation.subjectLabel }),
125
+ conversation.lastMessagePreview ? /* @__PURE__ */ jsx2("span", { className: "cv-p-row__preview", children: conversation.lastMessagePreview }) : null
126
+ ] }),
127
+ /* @__PURE__ */ jsxs2("span", { className: "cv-p-row__meta", children: [
128
+ conversation.lastMessageAt ? /* @__PURE__ */ jsx2("span", { children: formatRowTime(conversation.lastMessageAt, locale) }) : null,
129
+ conversation.unreadCount > 0 ? /* @__PURE__ */ jsxs2("span", { className: "cv-p-row__unread", children: [
130
+ /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: conversation.unreadCount }),
131
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-sr-only", children: formatParticipantLabel(labels.unreadCount, conversation.unreadCount) })
132
+ ] }) : null
133
+ ] })
134
+ ] });
135
+ }
136
+ function SectionHeading({ title, count }) {
137
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
138
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-section__title", children: title }),
139
+ /* @__PURE__ */ jsx2("span", { className: "cv-p-section__count", children: count })
140
+ ] });
141
+ }
142
+ function Section({ section, props }) {
143
+ const rows = section.conversations.map((conversation) => /* @__PURE__ */ jsx2(
144
+ Row,
145
+ {
146
+ conversation,
147
+ group: props.subjectGroups.find((group) => group.subjectType === conversation.subjectType),
148
+ labels: props.labels,
149
+ locale: props.locale,
150
+ onSelect: props.onSelect
151
+ },
152
+ `${conversation.subjectType}:${conversation.subjectId}`
153
+ ));
154
+ const heading = /* @__PURE__ */ jsx2(SectionHeading, { title: sectionTitle(section, props), count: section.conversations.length });
155
+ if (section.key === "closed") {
156
+ return /* @__PURE__ */ jsxs2("details", { className: "cv-p-section cv-p-section--closed", children: [
157
+ /* @__PURE__ */ jsx2("summary", { className: "cv-p-section__heading", children: heading }),
158
+ rows
159
+ ] });
160
+ }
161
+ const modifier = section.key === "awaiting" ? " cv-p-section--awaiting" : "";
162
+ return /* @__PURE__ */ jsxs2("section", { className: `cv-p-section${modifier}`, children: [
163
+ /* @__PURE__ */ jsx2("h3", { className: "cv-p-section__heading", children: heading }),
164
+ rows
165
+ ] });
166
+ }
167
+ function ParticipantInbox(props) {
168
+ const { view, labels, filter, onFilterChange } = props;
169
+ const loadView = resolveLoadView({ status: props.status, hasItems: view.sections.length > 0 });
170
+ return /* @__PURE__ */ jsxs2("div", { className: "cv-p cv-p-inbox", "aria-busy": loadView.isLoading, children: [
171
+ /* @__PURE__ */ jsx2("h2", { className: "cv-p-inbox__title", children: labels.inboxTitle }),
172
+ view.showFilters ? /* @__PURE__ */ jsx2(Filters, { filters: view.filters, active: filter, labels, onChange: onFilterChange }) : null,
173
+ loadView.isLoading ? /* @__PURE__ */ jsx2(ParticipantLoading, { labels }) : null,
174
+ loadView.hasError ? /* @__PURE__ */ jsx2(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
175
+ loadView.isEmpty ? /* @__PURE__ */ jsx2("p", { className: "cv-p-empty", children: labels.emptyInbox }) : null,
176
+ view.sections.map((section) => /* @__PURE__ */ jsx2(Section, { section, props }, section.key)),
177
+ props.hasMore ? /* @__PURE__ */ jsx2("button", { type: "button", className: "cv-p-button cv-p-inbox__more", onClick: props.loadMore, children: labels.loadMore }) : null
178
+ ] });
179
+ }
180
+
181
+ // src/participant/ParticipantThreadScreen.tsx
182
+ import { useEffect as useEffect7, useMemo as useMemo3, useState as useState6 } from "react";
183
+
184
+ // src/participant/ParticipantComposer.tsx
185
+ import { useRef, useState } from "react";
186
+
187
+ // src/participant/participantFileValidation.ts
188
+ function matchesAcceptedType(mimeType, acceptedTypes) {
189
+ return acceptedTypes.some(
190
+ (accepted) => accepted.endsWith("/*") ? mimeType.startsWith(accepted.slice(0, -1)) : mimeType === accepted
191
+ );
192
+ }
193
+ function rejectionOf(file, params) {
194
+ if (params.acceptedTypes && !matchesAcceptedType(file.type, params.acceptedTypes)) return "typeNotAccepted";
195
+ if (file.size > params.maxBytes) return "tooLarge";
196
+ return void 0;
197
+ }
198
+ function reduceFilesSelection(params) {
199
+ const files = [...params.current];
200
+ const rejected = [];
201
+ for (const file of params.incoming) {
202
+ const reason = rejectionOf(file, params);
203
+ if (reason === void 0) files.push(file);
204
+ else rejected.push({ file, reason });
205
+ }
206
+ return { files, rejected };
207
+ }
208
+ function maxBytesForChannel(channel = "app") {
209
+ return channelCapabilityFor(channel).attachments.maxBytes;
210
+ }
211
+
212
+ // src/participant/participantQuickReply.ts
213
+ function applyQuickReply(currentText, replyText) {
214
+ if (replyText === "") return currentText;
215
+ if (currentText.trim() === "") return replyText;
216
+ return /\s$/.test(currentText) ? `${currentText}${replyText}` : `${currentText} ${replyText}`;
217
+ }
218
+
219
+ // src/participant/ParticipantComposer.tsx
220
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
221
+ function QuickReplyStrip({ quickReplies, labels, onPick }) {
222
+ return /* @__PURE__ */ jsx3("div", { className: "cv-p-quick", role: "group", "aria-label": labels.quickRepliesGroup, children: quickReplies.map((quickReply) => /* @__PURE__ */ jsx3("button", { type: "button", className: "cv-p-chip", onClick: () => onPick(quickReply.body), children: quickReply.title }, quickReply.id)) });
223
+ }
224
+ function FileList({ files, labels, onRemove }) {
225
+ if (files.length === 0) return null;
226
+ return /* @__PURE__ */ jsx3("ul", { className: "cv-p-files", "aria-label": labels.attachmentsList, children: files.map((file, index) => /* @__PURE__ */ jsxs3("li", { className: "cv-p-files__item", children: [
227
+ /* @__PURE__ */ jsx3("span", { className: "cv-p-files__name", children: file.name }),
228
+ /* @__PURE__ */ jsx3("span", { className: "cv-p-files__size", children: formatFileSize(file.size) }),
229
+ /* @__PURE__ */ jsxs3("button", { type: "button", className: "cv-p-files__remove", "aria-label": `${labels.removeAttachment}: ${file.name}`, onClick: () => onRemove(file), children: [
230
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\xD7" }),
231
+ /* @__PURE__ */ jsx3("span", { className: "cv-p-sr-only", children: labels.removeAttachment })
232
+ ] })
233
+ ] }, `${file.name}-${file.size}-${index}`)) });
234
+ }
235
+ function rejectionText(rejection, labels) {
236
+ return rejection === "tooLarge" ? labels.attachmentTooLarge : labels.attachmentTypeNotAccepted;
237
+ }
238
+ function AttachControl({ labels, disabled, acceptedTypes, onChosen }) {
239
+ const fileInputRef = useRef(null);
240
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
241
+ /* @__PURE__ */ jsx3(
242
+ "input",
243
+ {
244
+ ref: fileInputRef,
245
+ className: "cv-p-composer__file-input",
246
+ type: "file",
247
+ multiple: true,
248
+ hidden: true,
249
+ accept: acceptedTypes?.join(","),
250
+ onChange: onChosen
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsxs3("button", { type: "button", className: "cv-p-button", disabled, onClick: () => fileInputRef.current?.click(), children: [
254
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\uFF0B" }),
255
+ /* @__PURE__ */ jsx3("span", { className: "cv-p-sr-only", children: labels.attach })
256
+ ] })
257
+ ] });
258
+ }
259
+ function useFileSelection(props) {
260
+ const [rejection, setRejection] = useState(void 0);
261
+ const { files, onFilesChange, channel } = props;
262
+ const maxBytes = maxBytesForChannel(channel);
263
+ function handleFilesChosen(event) {
264
+ const chosen = Array.from(event.target.files ?? []);
265
+ event.target.value = "";
266
+ const result = reduceFilesSelection({ current: files, incoming: chosen, maxBytes, acceptedTypes: props.acceptedTypes });
267
+ setRejection(result.rejected[0]?.reason);
268
+ if (result.files.length !== files.length) onFilesChange(result.files);
269
+ }
270
+ function handleRemoveFile(file) {
271
+ setRejection(void 0);
272
+ onFilesChange(files.filter((current) => current !== file));
273
+ }
274
+ return { rejection, handleFilesChosen, handleRemoveFile };
275
+ }
276
+ function ParticipantComposer(props) {
277
+ const { value, onChange, files, onSend, labels, channel = "app", disabled = false, isSending = false } = props;
278
+ const { rejection, handleFilesChosen, handleRemoveFile } = useFileSelection(props);
279
+ const canSend = !disabled && !isSending && (value.trim().length > 0 || files.length > 0);
280
+ function handleSubmit(event) {
281
+ event.preventDefault();
282
+ if (canSend) onSend();
283
+ }
284
+ return /* @__PURE__ */ jsxs3("form", { className: "cv-p-composer", onSubmit: handleSubmit, children: [
285
+ props.quickReplies && props.quickReplies.length > 0 ? /* @__PURE__ */ jsx3(QuickReplyStrip, { quickReplies: props.quickReplies, labels, onPick: (body) => onChange(applyQuickReply(value, body)) }) : null,
286
+ /* @__PURE__ */ jsx3(FileList, { files, labels, onRemove: handleRemoveFile }),
287
+ rejection ? /* @__PURE__ */ jsx3("p", { className: "cv-p-composer__error", role: "alert", children: rejectionText(rejection, labels) }) : null,
288
+ /* @__PURE__ */ jsxs3("div", { className: "cv-p-composer__row", children: [
289
+ channelCapabilityFor(channel).attachments.accepted ? /* @__PURE__ */ jsx3(AttachControl, { labels, disabled, acceptedTypes: props.acceptedTypes, onChosen: handleFilesChosen }) : null,
290
+ /* @__PURE__ */ jsx3(
291
+ "textarea",
292
+ {
293
+ className: "cv-p-composer__input",
294
+ "aria-label": labels.messageInputLabel,
295
+ placeholder: labels.messageInputPlaceholder,
296
+ rows: 1,
297
+ value,
298
+ maxLength: props.maxLength,
299
+ disabled,
300
+ onChange: (event) => onChange(event.target.value)
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsxs3("button", { type: "submit", className: "cv-p-button cv-p-button--primary", disabled: !canSend, children: [
304
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u27A4" }),
305
+ /* @__PURE__ */ jsx3("span", { className: "cv-p-sr-only", children: labels.send })
306
+ ] })
307
+ ] })
308
+ ] });
309
+ }
310
+
311
+ // src/participant/participantBubbleActions.ts
312
+ function bindPendingActions(actions, clientMessageId) {
313
+ const { retryHost } = actions;
314
+ return {
315
+ retryLocal: () => actions.retryLocal(clientMessageId),
316
+ discardLocal: () => actions.discardLocal(clientMessageId),
317
+ editLocal: () => actions.editLocal(clientMessageId),
318
+ ...retryHost ? { retryHost: () => retryHost(clientMessageId) } : {}
319
+ };
320
+ }
321
+ function resolveParticipantBubbleActions(item, handlers) {
322
+ if (item.kind === "server" || item.displayState !== "failed") return {};
323
+ if (item.origin === "host") return handlers.retryHost ? { onRetry: handlers.retryHost } : {};
324
+ return { onRetry: handlers.retryLocal, onDiscard: handlers.discardLocal, onEdit: handlers.editLocal };
325
+ }
326
+
327
+ // src/participant/ParticipantAttachmentItem.tsx
328
+ import { useEffect, useState as useState2 } from "react";
329
+
330
+ // src/participant/participantAttachmentUrl.ts
331
+ var SAFE_ATTACHMENT_PROTOCOLS = /* @__PURE__ */ new Set(["https:", "http:", "blob:"]);
332
+ function isSafeAttachmentUrl(url) {
333
+ try {
334
+ return SAFE_ATTACHMENT_PROTOCOLS.has(new URL(url.trim()).protocol);
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+
340
+ // src/participant/ParticipantAttachmentItem.tsx
341
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
342
+ function useAttachmentUrl(attachment, resolveAttachmentUrl) {
343
+ const [url, setUrl] = useState2(void 0);
344
+ const disposition = attachment.kind === "document" ? "attachment" : "inline";
345
+ useEffect(() => {
346
+ let isCurrent = true;
347
+ resolveAttachmentUrl(attachment, disposition).then(
348
+ (resolved) => {
349
+ if (isCurrent) setUrl(isSafeAttachmentUrl(resolved) ? resolved.trim() : void 0);
350
+ },
351
+ () => {
352
+ if (isCurrent) setUrl(void 0);
353
+ }
354
+ );
355
+ return () => {
356
+ isCurrent = false;
357
+ };
358
+ }, [attachment, disposition, resolveAttachmentUrl]);
359
+ return url;
360
+ }
361
+ function ParticipantAttachmentItem({ attachment, resolveAttachmentUrl, labels }) {
362
+ const url = useAttachmentUrl(attachment, resolveAttachmentUrl);
363
+ if (attachment.kind === "image") {
364
+ return /* @__PURE__ */ jsxs4("figure", { className: "cv-p-attachment cv-p-attachment--image", children: [
365
+ url ? /* @__PURE__ */ jsx4("img", { className: "cv-p-attachment__image", src: url, alt: attachment.filename }) : null,
366
+ /* @__PURE__ */ jsx4("figcaption", { className: "cv-p-attachment__name", children: attachment.filename })
367
+ ] });
368
+ }
369
+ if (attachment.kind === "audio") {
370
+ return /* @__PURE__ */ jsxs4("div", { className: "cv-p-attachment cv-p-attachment--audio", children: [
371
+ /* @__PURE__ */ jsx4("span", { className: "cv-p-attachment__name", children: attachment.filename }),
372
+ /* @__PURE__ */ jsx4("audio", { className: "cv-p-attachment__audio", controls: true, preload: "none", src: url })
373
+ ] });
374
+ }
375
+ return /* @__PURE__ */ jsxs4(
376
+ "a",
377
+ {
378
+ className: "cv-p-attachment cv-p-attachment--document",
379
+ href: url,
380
+ download: attachment.filename,
381
+ "aria-label": `${labels.downloadAttachment}: ${attachment.filename}`,
382
+ children: [
383
+ /* @__PURE__ */ jsx4("span", { className: "cv-p-attachment__name", children: attachment.filename }),
384
+ /* @__PURE__ */ jsx4("span", { className: "cv-p-attachment__size", children: formatFileSize(attachment.sizeBytes) })
385
+ ]
386
+ }
387
+ );
388
+ }
389
+
390
+ // src/participant/participantMessages.ts
391
+ function isSameSubject(left, right) {
392
+ return left.subjectType === right.subjectType && left.subjectId === right.subjectId;
393
+ }
394
+ function createdAtOf(item) {
395
+ return Date.parse(item.kind === "server" ? item.message.createdAt : item.pending.createdAt);
396
+ }
397
+ function mergeParticipantMessages(params) {
398
+ const { serverMessages, hostPending, localPending, subject } = params;
399
+ const knownClientIds = /* @__PURE__ */ new Set();
400
+ for (const message of serverMessages) {
401
+ if (message.clientMessageId) knownClientIds.add(message.clientMessageId);
402
+ }
403
+ const items = serverMessages.map((message) => ({ kind: "server", message }));
404
+ for (const pending of hostPending) {
405
+ if (!isSameSubject(pending.subject, subject) || knownClientIds.has(pending.clientMessageId)) continue;
406
+ knownClientIds.add(pending.clientMessageId);
407
+ items.push({ kind: "pending", pending, origin: "host", displayState: pending.state });
408
+ }
409
+ for (const pending of localPending) {
410
+ if (!isSameSubject(pending.subject, subject) || knownClientIds.has(pending.clientMessageId)) continue;
411
+ items.push({ kind: "pending", pending, origin: "local", displayState: pending.state });
412
+ }
413
+ return items.sort((left, right) => createdAtOf(left) - createdAtOf(right));
414
+ }
415
+ function isOwnMessage(message) {
416
+ return message.direction === "inbound";
417
+ }
418
+ function resolveOwnMessageStatus(params) {
419
+ const { displayState, serverStatus, confirmsRead } = params;
420
+ if (displayState) return displayState;
421
+ if (serverStatus === void 0) return "sent";
422
+ if (serverStatus === "failed" || serverStatus === "bounced") return "failed";
423
+ if (serverStatus === "read") return confirmsRead ? "read" : "delivered";
424
+ return serverStatus;
425
+ }
426
+
427
+ // src/participant/ParticipantMessageBubble.tsx
428
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
429
+ function describeItem(item) {
430
+ if (item.kind === "server") {
431
+ const { message } = item;
432
+ return {
433
+ isMine: isOwnMessage(message),
434
+ authorName: message.authorName ?? void 0,
435
+ text: message.text ?? void 0,
436
+ createdAt: message.createdAt,
437
+ attachments: message.attachments,
438
+ pendingFilenames: [],
439
+ serverStatus: message.status
440
+ };
441
+ }
442
+ return {
443
+ isMine: true,
444
+ text: item.pending.text,
445
+ createdAt: item.pending.createdAt,
446
+ attachments: [],
447
+ pendingFilenames: (item.pending.attachments ?? []).map((attachment) => attachment.filename),
448
+ displayState: item.displayState
449
+ };
450
+ }
451
+ function statusText(status, labels) {
452
+ const byStatus = {
453
+ sending: labels.statusSending,
454
+ queued: labels.statusQueued,
455
+ sent: labels.statusSent,
456
+ delivered: labels.statusDelivered,
457
+ read: labels.statusRead,
458
+ failed: labels.statusFailed
459
+ };
460
+ return byStatus[status];
461
+ }
462
+ function OwnStatus({ status, labels, onRetry }) {
463
+ const ticksStatus = status === "sending" ? "queued" : status;
464
+ const isVisibleText = status === "sending" || status === "queued" || status === "failed";
465
+ if (status === "failed" && onRetry) {
466
+ return /* @__PURE__ */ jsxs5("button", { type: "button", className: "cv-p-bubble__retry", onClick: onRetry, children: [
467
+ /* @__PURE__ */ jsx5(StatusTicks, { status: "failed" }),
468
+ /* @__PURE__ */ jsx5("span", { children: labels.statusFailed })
469
+ ] });
470
+ }
471
+ const text = status === "failed" ? labels.statusFailedShort : statusText(status, labels);
472
+ return /* @__PURE__ */ jsxs5("span", { className: "cv-p-bubble__status", children: [
473
+ /* @__PURE__ */ jsx5(StatusTicks, { status: ticksStatus }),
474
+ /* @__PURE__ */ jsx5("span", { className: isVisibleText ? "cv-p-bubble__status-text" : "cv-p-sr-only", children: text })
475
+ ] });
476
+ }
477
+ function FailedActions({ labels, onDiscard, onEdit }) {
478
+ if (!onDiscard && !onEdit) return null;
479
+ return /* @__PURE__ */ jsxs5("span", { className: "cv-p-bubble__actions", children: [
480
+ onEdit ? /* @__PURE__ */ jsx5("button", { type: "button", className: "cv-p-bubble__action", onClick: onEdit, children: labels.edit }) : null,
481
+ onDiscard ? /* @__PURE__ */ jsx5("button", { type: "button", className: "cv-p-bubble__action", onClick: onDiscard, children: labels.discard }) : null
482
+ ] });
483
+ }
484
+ function toTextPayload(text, createdAt, isMine) {
485
+ return {
486
+ id: createdAt,
487
+ type: "text",
488
+ content: text,
489
+ direction: isMine ? "inbound" : "outbound",
490
+ sender: isMine ? "customer" : "agent",
491
+ timestamp: createdAt
492
+ };
493
+ }
494
+ function ParticipantMessageBubble({
495
+ item,
496
+ labels,
497
+ confirmsRead,
498
+ resolveAttachmentUrl,
499
+ onRetry,
500
+ onDiscard,
501
+ onEdit
502
+ }) {
503
+ const content = describeItem(item);
504
+ const { isMine } = content;
505
+ const ownStatus = isMine ? resolveOwnMessageStatus({ displayState: content.displayState, serverStatus: content.serverStatus, confirmsRead }) : void 0;
506
+ return /* @__PURE__ */ jsxs5("div", { className: isMine ? "cv-p-bubble cv-p-bubble--mine" : "cv-p-bubble", children: [
507
+ isMine ? /* @__PURE__ */ jsx5("span", { className: "cv-p-sr-only", children: labels.me }) : content.authorName ? /* @__PURE__ */ jsx5("span", { className: "cv-p-bubble__author", children: content.authorName }) : null,
508
+ content.text ? /* @__PURE__ */ jsx5(MessageText, { message: toTextPayload(content.text, content.createdAt, isMine), copyOnClick: false }) : null,
509
+ content.attachments.map((attachment) => /* @__PURE__ */ jsx5(
510
+ ParticipantAttachmentItem,
511
+ {
512
+ attachment,
513
+ resolveAttachmentUrl,
514
+ labels
515
+ },
516
+ attachment.id
517
+ )),
518
+ content.pendingFilenames.map((filename, index) => /* @__PURE__ */ jsx5("span", { className: "cv-p-attachment cv-p-attachment--pending", children: filename }, `${filename}-${index}`)),
519
+ /* @__PURE__ */ jsxs5("span", { className: "cv-p-bubble__meta", children: [
520
+ /* @__PURE__ */ jsx5("time", { className: "cv-p-bubble__time", dateTime: content.createdAt, children: formatTimestamp(content.createdAt) }),
521
+ ownStatus ? /* @__PURE__ */ jsx5(OwnStatus, { status: ownStatus, labels, onRetry }) : null
522
+ ] }),
523
+ ownStatus === "failed" ? /* @__PURE__ */ jsx5(FailedActions, { labels, onDiscard, onEdit }) : null
524
+ ] });
525
+ }
526
+
527
+ // src/participant/ParticipantThread.tsx
528
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
529
+ function createdAtOf2(item) {
530
+ return item.kind === "server" ? item.message.createdAt : item.pending.createdAt;
531
+ }
532
+ function keyOf(item) {
533
+ return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
534
+ }
535
+ function resolveActions(item, pendingActions) {
536
+ if (!pendingActions || item.kind === "server") return {};
537
+ return resolveParticipantBubbleActions(item, bindPendingActions(pendingActions, item.pending.clientMessageId));
538
+ }
539
+ function Header({ props }) {
540
+ const { conversation, labels, onBack, onOpenSubject } = props;
541
+ const { subjectType, subjectId } = conversation;
542
+ return /* @__PURE__ */ jsxs6("header", { className: "cv-p-thread__header", children: [
543
+ onBack ? /* @__PURE__ */ jsxs6("button", { type: "button", className: "cv-p-button cv-p-thread__back", onClick: onBack, children: [
544
+ /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: "\u2039" }),
545
+ /* @__PURE__ */ jsx6("span", { className: "cv-p-sr-only", children: labels.back })
546
+ ] }) : null,
547
+ /* @__PURE__ */ jsx6("h2", { className: "cv-p-thread__title", children: onOpenSubject ? /* @__PURE__ */ jsx6(
548
+ "button",
549
+ {
550
+ type: "button",
551
+ className: "cv-p-thread__title-button",
552
+ "aria-label": `${labels.openSubject}: ${conversation.subjectLabel}`,
553
+ onClick: () => onOpenSubject({ subjectType, subjectId }),
554
+ children: conversation.subjectLabel
555
+ }
556
+ ) : conversation.subjectLabel })
557
+ ] });
558
+ }
559
+ function Timeline({ props }) {
560
+ const { items, labels, locale, resolveAttachmentUrl, pendingActions } = props;
561
+ const confirmsRead = channelCapabilityFor(props.channel ?? "app").confirmsRead;
562
+ const nodes = [];
563
+ let previousDay;
564
+ for (const item of items) {
565
+ const day = new Date(createdAtOf2(item));
566
+ if (previousDay === void 0 || !isSameDay(previousDay, day)) {
567
+ nodes.push(
568
+ /* @__PURE__ */ jsx6("div", { className: "cv-p-day", role: "separator", children: day.toLocaleDateString(locale, { weekday: "short", day: "2-digit", month: "2-digit" }) }, `day:${keyOf(item)}`)
569
+ );
570
+ }
571
+ previousDay = day;
572
+ nodes.push(
573
+ /* @__PURE__ */ jsx6(
574
+ ParticipantMessageBubble,
575
+ {
576
+ item,
577
+ labels,
578
+ confirmsRead,
579
+ resolveAttachmentUrl,
580
+ ...resolveActions(item, pendingActions)
581
+ },
582
+ keyOf(item)
583
+ )
584
+ );
585
+ }
586
+ return /* @__PURE__ */ jsx6("div", { className: "cv-p-thread__messages", children: nodes });
587
+ }
588
+ function Footer({ props }) {
589
+ const { conversation, labels, draft } = props;
590
+ if (conversation.status === "closed") {
591
+ return /* @__PURE__ */ jsx6("p", { className: "cv-p-thread__closed", children: labels.closedNotice });
592
+ }
593
+ return /* @__PURE__ */ jsx6(
594
+ ParticipantComposer,
595
+ {
596
+ value: draft.value,
597
+ onChange: draft.onChange,
598
+ files: draft.files,
599
+ onFilesChange: draft.onFilesChange,
600
+ onSend: props.onSend,
601
+ labels,
602
+ channel: props.channel,
603
+ isSending: props.isSending,
604
+ maxLength: props.maxLength,
605
+ acceptedTypes: props.acceptedTypes,
606
+ quickReplies: props.quickReplies
607
+ }
608
+ );
609
+ }
610
+ function ParticipantThread(props) {
611
+ const { conversation, labels, hasMore, onLoadOlder, renderSubjectCard } = props;
612
+ const subjectCard = renderSubjectCard?.(conversation);
613
+ const loadView = resolveLoadView({ status: props.status, hasItems: props.items.length > 0 });
614
+ return /* @__PURE__ */ jsxs6("div", { className: "cv-p cv-p-thread", "aria-busy": loadView.isLoading, children: [
615
+ /* @__PURE__ */ jsx6(Header, { props }),
616
+ subjectCard ? /* @__PURE__ */ jsx6("div", { className: "cv-p-thread__subject-card", children: subjectCard }) : null,
617
+ /* @__PURE__ */ jsxs6("div", { className: "cv-p-thread__scroll", ref: props.scroll.ref, onScroll: props.scroll.onScroll, children: [
618
+ loadView.isLoading ? /* @__PURE__ */ jsx6(ParticipantLoading, { labels }) : null,
619
+ loadView.hasError ? /* @__PURE__ */ jsx6(ParticipantLoadError, { labels, onRetry: props.refresh }) : null,
620
+ hasMore && onLoadOlder ? /* @__PURE__ */ jsx6("button", { type: "button", className: "cv-p-thread__older", onClick: onLoadOlder, children: labels.loadOlder }) : null,
621
+ /* @__PURE__ */ jsx6(Timeline, { props })
622
+ ] }),
623
+ /* @__PURE__ */ jsx6("div", { className: "cv-p-thread__live", role: "status", "aria-live": "polite", children: props.newMessagesCount > 0 ? labels.newMessages : "" }),
624
+ /* @__PURE__ */ jsx6(Footer, { props })
625
+ ] });
626
+ }
627
+
628
+ // src/participant/bindAttachmentUrlResolver.ts
629
+ function bindAttachmentUrlResolver(api) {
630
+ return (attachment, disposition) => api.resolveAttachmentUrl(attachment, disposition);
631
+ }
632
+
633
+ // src/participant/participantSendController.ts
634
+ function createParticipantSendEntry(params) {
635
+ const { clientMessageId, subject, content, createdAt, state = "sending" } = params;
636
+ return {
637
+ clientMessageId,
638
+ subject,
639
+ ...content.text ? { text: content.text } : {},
640
+ files: params.files ?? content.files ?? [],
641
+ createdAt,
642
+ state
643
+ };
644
+ }
645
+ function setState(entries, clientMessageId, allowedFrom, next) {
646
+ const target = entries.find((item) => item.clientMessageId === clientMessageId);
647
+ if (!target || !allowedFrom.includes(target.state) || target.state === next) return entries;
648
+ return entries.map((item) => item === target ? { ...item, state: next } : item);
649
+ }
650
+ function markSent(entries, clientMessageId, message) {
651
+ const target = entries.find((item) => item.clientMessageId === clientMessageId);
652
+ if (!target || target.state === "sent") return entries;
653
+ return entries.map((item) => item === target ? { ...item, state: "sent", sentMessage: message } : item);
654
+ }
655
+ function remove(entries, shouldRemove) {
656
+ const kept = entries.filter((item) => !shouldRemove(item));
657
+ return kept.length === entries.length ? entries : kept;
658
+ }
659
+ function participantSendReducer(entries, action) {
660
+ switch (action.type) {
661
+ case "started":
662
+ return [...entries.filter((item) => item.clientMessageId !== action.entry.clientMessageId), action.entry];
663
+ case "discarded":
664
+ return remove(entries, (item) => item.clientMessageId === action.clientMessageId);
665
+ case "sent":
666
+ return markSent(entries, action.clientMessageId, action.message);
667
+ case "queued":
668
+ return setState(entries, action.clientMessageId, ["sending"], "queued");
669
+ case "failed":
670
+ return setState(entries, action.clientMessageId, ["sending", "queued"], "failed");
671
+ case "retried":
672
+ return setState(entries, action.clientMessageId, ["failed"], "sending");
673
+ case "reflected":
674
+ return remove(entries, (item) => action.knownClientMessageIds.has(item.clientMessageId));
675
+ }
676
+ }
677
+ function findFailedEntry(entries, clientMessageId) {
678
+ return entries.find((item) => item.clientMessageId === clientMessageId && item.state === "failed");
679
+ }
680
+ function hasSendingEntry(entries) {
681
+ return entries.some((item) => item.state === "sending");
682
+ }
683
+ function collectKnownClientMessageIds(params) {
684
+ const known = /* @__PURE__ */ new Set();
685
+ for (const message of params.messages) {
686
+ if (message.clientMessageId) known.add(message.clientMessageId);
687
+ }
688
+ for (const pending of params.hostPending) known.add(pending.clientMessageId);
689
+ return known;
690
+ }
691
+ function toLocalPending(entries) {
692
+ return entries.map((item) => {
693
+ const attachments = item.files.map((file) => ({ filename: file.name, mimeType: file.type, sizeBytes: file.size }));
694
+ return {
695
+ clientMessageId: item.clientMessageId,
696
+ subject: item.subject,
697
+ createdAt: item.createdAt,
698
+ state: item.state,
699
+ ...item.text ? { text: item.text } : {},
700
+ ...attachments.length > 0 ? { attachments } : {}
701
+ };
702
+ });
703
+ }
704
+ function appendEntryToDraft(entry, current) {
705
+ const text = [entry.text, current.text].filter((part) => Boolean(part)).join("\n");
706
+ return { text, files: [...entry.files, ...current.files] };
707
+ }
708
+
709
+ // src/participant/useParticipantDraft.ts
710
+ import { useState as useState3 } from "react";
711
+
712
+ // src/participant/participantDrafts.ts
713
+ var EMPTY_PARTICIPANT_DRAFT = { text: "", files: [] };
714
+ function draftKey(subject) {
715
+ return `${subject.subjectType}:${subject.subjectId}`;
716
+ }
717
+ function getDraft(drafts, subject) {
718
+ return drafts.get(draftKey(subject)) ?? EMPTY_PARTICIPANT_DRAFT;
719
+ }
720
+ function setDraft(drafts, subject, draft) {
721
+ return new Map(drafts).set(draftKey(subject), draft);
722
+ }
723
+ function clearDraft(drafts, subject) {
724
+ const next = new Map(drafts);
725
+ next.delete(draftKey(subject));
726
+ return next;
727
+ }
728
+ function takeDraftForSend(drafts, subject) {
729
+ const draft = getDraft(drafts, subject);
730
+ const text = draft.text.trim();
731
+ if (!text && draft.files.length === 0) return { drafts };
732
+ return {
733
+ drafts: clearDraft(drafts, subject),
734
+ content: { ...text ? { text } : {}, ...draft.files.length > 0 ? { files: draft.files } : {} }
735
+ };
736
+ }
737
+
738
+ // src/participant/useParticipantDraft.ts
739
+ function useParticipantDraft(draftsRef, subject) {
740
+ const [draft, setDraftState] = useState3(() => getDraft(draftsRef.current, subject));
741
+ function replaceDrafts(next) {
742
+ draftsRef.current = next;
743
+ setDraftState(getDraft(next, subject));
744
+ }
745
+ function update(change) {
746
+ replaceDrafts(setDraft(draftsRef.current, subject, { ...getDraft(draftsRef.current, subject), ...change }));
747
+ }
748
+ function takeForSend() {
749
+ const taken = takeDraftForSend(draftsRef.current, subject);
750
+ if (taken.content) replaceDrafts(taken.drafts);
751
+ return taken.content;
752
+ }
753
+ function restoreFromEntry(entry) {
754
+ update(appendEntryToDraft(entry, getDraft(draftsRef.current, subject)));
755
+ }
756
+ return { draft, setText: (text) => update({ text }), setFiles: (files) => update({ files }), takeForSend, restoreFromEntry };
757
+ }
758
+
759
+ // src/participant/useParticipantConversationView.ts
760
+ import { useCallback as useCallback3, useMemo as useMemo2 } from "react";
761
+
762
+ // src/participant/participantSendStates.ts
763
+ var NO_ENTRIES = [];
764
+ function selectSendEntries(states, subject) {
765
+ return states.get(draftKey(subject)) ?? NO_ENTRIES;
766
+ }
767
+ function participantSendStatesReducer(states, { subject, action }) {
768
+ const current = selectSendEntries(states, subject);
769
+ const next = participantSendReducer(current, action);
770
+ if (next === current) return states;
771
+ const copy = new Map(states);
772
+ if (next.length === 0) copy.delete(draftKey(subject));
773
+ else copy.set(draftKey(subject), next);
774
+ return copy;
775
+ }
776
+
777
+ // src/participant/useParticipantThread.ts
778
+ import { useCallback as useCallback2, useEffect as useEffect5, useMemo, useReducer, useRef as useRef4 } from "react";
779
+
780
+ // src/participant/participantThreadState.ts
781
+ var INITIAL_PARTICIPANT_THREAD_STATE = {
782
+ status: "idle",
783
+ messages: [],
784
+ hasMore: false
785
+ };
786
+ function unionById(current, incoming) {
787
+ const byId = new Map(current.map((message) => [message.id, message]));
788
+ for (const message of incoming) byId.set(message.id, message);
789
+ return [...byId.values()].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt));
790
+ }
791
+ function mergeMessages(state, incoming, hasMore) {
792
+ const messages = unionById(state.messages, incoming);
793
+ const { error: _error, ...rest } = state;
794
+ return { ...rest, status: "ready", messages, hasMore };
795
+ }
796
+ function participantThreadReducer(state, action) {
797
+ switch (action.type) {
798
+ case "reset":
799
+ return INITIAL_PARTICIPANT_THREAD_STATE;
800
+ case "started":
801
+ return state.status === "ready" ? state : { ...state, status: "loading" };
802
+ case "loaded":
803
+ return mergeMessages(state, action.messages, state.messages.length === 0 ? action.hasMore : state.hasMore);
804
+ case "olderLoaded":
805
+ return mergeMessages(state, action.messages, action.hasMore);
806
+ case "failed":
807
+ return { ...state, status: "error", error: action.error };
808
+ case "sendConfirmed":
809
+ return { ...state, messages: unionById(state.messages, [action.message]) };
810
+ }
811
+ }
812
+
813
+ // src/participant/useParticipantMarkRead.ts
814
+ import { useEffect as useEffect3 } from "react";
815
+
816
+ // src/participant/shouldMarkRead.ts
817
+ function shouldMarkParticipantRead(params) {
818
+ const { selected, subject, visibilityState, unreadCount } = params;
819
+ if (!selected) return false;
820
+ if (selected.subjectType !== subject.subjectType || selected.subjectId !== subject.subjectId) return false;
821
+ if (visibilityState !== "visible") return false;
822
+ return unreadCount > 0;
823
+ }
824
+
825
+ // src/participant/useDocumentVisibility.ts
826
+ import { useEffect as useEffect2, useState as useState4 } from "react";
827
+ function useDocumentVisibility() {
828
+ const [visibilityState, setVisibilityState] = useState4(
829
+ () => typeof document === "undefined" ? "visible" : document.visibilityState
830
+ );
831
+ useEffect2(() => {
832
+ function handleVisibilityChange() {
833
+ setVisibilityState(document.visibilityState);
834
+ }
835
+ handleVisibilityChange();
836
+ document.addEventListener("visibilitychange", handleVisibilityChange);
837
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
838
+ }, []);
839
+ return visibilityState;
840
+ }
841
+
842
+ // src/participant/useParticipantMarkRead.ts
843
+ function useParticipantMarkRead(params) {
844
+ const { apiRef, subject, unreadCount, messageCount, onMarkedRead } = params;
845
+ const visibilityState = useDocumentVisibility();
846
+ useEffect3(() => {
847
+ if (!shouldMarkParticipantRead({ selected: subject, subject, visibilityState, unreadCount })) return;
848
+ void apiRef.current.markRead(subject).then(() => onMarkedRead?.(subject), () => void 0);
849
+ }, [apiRef, subject, visibilityState, unreadCount, messageCount, onMarkedRead]);
850
+ }
851
+
852
+ // src/participant/useParticipantRevalidation.ts
853
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
854
+ function useParticipantRevalidation(api, onRevalidate) {
855
+ const handlerRef = useRef2(onRevalidate);
856
+ handlerRef.current = onRevalidate;
857
+ useEffect4(() => {
858
+ function handleFocus() {
859
+ handlerRef.current();
860
+ }
861
+ if (api.subscribe) {
862
+ const unsubscribe = api.subscribe((event) => handlerRef.current(event));
863
+ window.addEventListener("online", handleFocus);
864
+ return () => {
865
+ unsubscribe();
866
+ window.removeEventListener("online", handleFocus);
867
+ };
868
+ }
869
+ function handleVisibilityChange() {
870
+ if (document.visibilityState === "visible") handlerRef.current();
871
+ }
872
+ window.addEventListener("focus", handleFocus);
873
+ window.addEventListener("online", handleFocus);
874
+ document.addEventListener("visibilitychange", handleVisibilityChange);
875
+ return () => {
876
+ window.removeEventListener("focus", handleFocus);
877
+ window.removeEventListener("online", handleFocus);
878
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
879
+ };
880
+ }, [api]);
881
+ }
882
+
883
+ // src/participant/useParticipantSend.ts
884
+ import { useCallback, useRef as useRef3 } from "react";
885
+ function useParticipantSend(params) {
886
+ const { apiRef, subject, dispatchSend, onSentMessage } = params;
887
+ const entriesRef = useRef3(params.entries);
888
+ entriesRef.current = params.entries;
889
+ const deliver = useCallback(
890
+ async (entry) => {
891
+ const { clientMessageId } = entry;
892
+ try {
893
+ const result = await apiRef.current.sendMessage({
894
+ subject: entry.subject,
895
+ clientMessageId,
896
+ ...entry.text ? { text: entry.text } : {},
897
+ ...entry.files.length > 0 ? { files: entry.files } : {}
898
+ });
899
+ if (result.outcome === "sent") {
900
+ onSentMessage(result.message);
901
+ dispatchSend({ type: "sent", clientMessageId, message: result.message });
902
+ return "sent";
903
+ }
904
+ dispatchSend({ type: "queued", clientMessageId });
905
+ return "queued";
906
+ } catch {
907
+ dispatchSend({ type: "failed", clientMessageId });
908
+ return "failed";
909
+ }
910
+ },
911
+ [apiRef, dispatchSend, onSentMessage]
912
+ );
913
+ const send = useCallback(
914
+ async (content) => {
915
+ const entry = createParticipantSendEntry({
916
+ clientMessageId: crypto.randomUUID(),
917
+ subject,
918
+ content,
919
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
920
+ });
921
+ dispatchSend({ type: "started", entry });
922
+ return deliver(entry);
923
+ },
924
+ [deliver, dispatchSend, subject]
925
+ );
926
+ const retry = useCallback(
927
+ async (clientMessageId) => {
928
+ const entry = findFailedEntry(entriesRef.current, clientMessageId);
929
+ if (!entry) return;
930
+ dispatchSend({ type: "retried", clientMessageId });
931
+ await deliver({ ...entry, state: "sending" });
932
+ },
933
+ [deliver, dispatchSend]
934
+ );
935
+ return { send, retry };
936
+ }
937
+
938
+ // src/participant/useParticipantThread.ts
939
+ var THREAD_PAGE_SIZE = 30;
940
+ function describeError(error) {
941
+ return error instanceof Error ? error.message : "unknown error";
942
+ }
943
+ function isForSubject(event, subject) {
944
+ if (!event || event.type === "inbox-changed") return true;
945
+ return event.subject.subjectType === subject.subjectType && event.subject.subjectId === subject.subjectId;
946
+ }
947
+ function useParticipantThread(params) {
948
+ const { api, subject, channel, unreadCount = 0, onMarkedRead, sendEntries, dispatchSend, hostPending = [] } = params;
949
+ const { subjectType, subjectId } = subject;
950
+ const stableSubject = useMemo(() => ({ subjectType, subjectId }), [subjectType, subjectId]);
951
+ const [state, dispatch] = useReducer(participantThreadReducer, INITIAL_PARTICIPANT_THREAD_STATE);
952
+ const apiRef = useRef4(api);
953
+ apiRef.current = api;
954
+ const activeSubjectKey = useRef4("");
955
+ const confirmsRead = channelCapabilityFor(channel).confirmsRead;
956
+ const refresh = useCallback2(async () => {
957
+ const requestKey = `${subjectType}:${subjectId}`;
958
+ dispatch({ type: "started" });
959
+ try {
960
+ const messages = await apiRef.current.fetchMessages(stableSubject, { limit: THREAD_PAGE_SIZE });
961
+ if (activeSubjectKey.current === requestKey) {
962
+ dispatch({ type: "loaded", messages, hasMore: messages.length >= THREAD_PAGE_SIZE });
963
+ }
964
+ } catch (error) {
965
+ if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
966
+ }
967
+ }, [stableSubject, subjectType, subjectId]);
968
+ const oldestMessageId = state.messages[0]?.id;
969
+ const loadOlder = useCallback2(async () => {
970
+ if (!oldestMessageId) return;
971
+ const requestKey = `${subjectType}:${subjectId}`;
972
+ try {
973
+ const messages = await apiRef.current.fetchMessages(stableSubject, { before: oldestMessageId, limit: THREAD_PAGE_SIZE });
974
+ if (activeSubjectKey.current === requestKey) {
975
+ dispatch({ type: "olderLoaded", messages, hasMore: messages.length >= THREAD_PAGE_SIZE });
976
+ }
977
+ } catch (error) {
978
+ if (activeSubjectKey.current === requestKey) dispatch({ type: "failed", error: describeError(error) });
979
+ }
980
+ }, [stableSubject, subjectType, subjectId, oldestMessageId]);
981
+ const handleSentMessage = useCallback2((message) => dispatch({ type: "sendConfirmed", message }), []);
982
+ const sending = useParticipantSend({ apiRef, subject: stableSubject, entries: sendEntries, dispatchSend, onSentMessage: handleSentMessage });
983
+ useEffect5(() => {
984
+ activeSubjectKey.current = `${subjectType}:${subjectId}`;
985
+ dispatch({ type: "reset" });
986
+ void refresh();
987
+ }, [refresh, subjectType, subjectId]);
988
+ useParticipantRevalidation(api, (event) => {
989
+ if (isForSubject(event, stableSubject)) void refresh();
990
+ });
991
+ useEffect5(() => {
992
+ if (sendEntries.length === 0) return;
993
+ dispatchSend({ type: "reflected", knownClientMessageIds: collectKnownClientMessageIds({ messages: state.messages, hostPending }) });
994
+ }, [sendEntries, state.messages, hostPending, dispatchSend]);
995
+ useParticipantMarkRead({ apiRef, subject: stableSubject, unreadCount, messageCount: state.messages.length, onMarkedRead });
996
+ const localPending = useMemo(() => toLocalPending(sendEntries), [sendEntries]);
997
+ return { ...state, localPending, confirmsRead, refresh, loadOlder, ...sending };
998
+ }
999
+
1000
+ // src/participant/useParticipantConversationView.ts
1001
+ function useParticipantConversationView(params) {
1002
+ const { api, subject, conversation, pendingMessages, sendStates, dispatchSendStates } = params;
1003
+ const { subjectType, subjectId } = subject;
1004
+ const sendEntries = selectSendEntries(sendStates, subject);
1005
+ const dispatchSend = useCallback3(
1006
+ (action) => dispatchSendStates({ subject: { subjectType, subjectId }, action }),
1007
+ [dispatchSendStates, subjectType, subjectId]
1008
+ );
1009
+ const thread = useParticipantThread({
1010
+ api,
1011
+ subject,
1012
+ channel: params.channel,
1013
+ unreadCount: conversation.unreadCount,
1014
+ onMarkedRead: params.onMarkedRead,
1015
+ sendEntries,
1016
+ dispatchSend,
1017
+ hostPending: pendingMessages
1018
+ });
1019
+ const items = useMemo2(
1020
+ () => mergeParticipantMessages({
1021
+ serverMessages: thread.messages,
1022
+ hostPending: pendingMessages ?? [],
1023
+ localPending: thread.localPending,
1024
+ subject
1025
+ }),
1026
+ [thread.messages, thread.localPending, pendingMessages, subject]
1027
+ );
1028
+ return { thread, items, sendEntries, dispatchSend };
1029
+ }
1030
+
1031
+ // src/participant/useParticipantThreadScroll.ts
1032
+ import { useEffect as useEffect6, useRef as useRef6 } from "react";
1033
+
1034
+ // src/participant/participantNewMessages.ts
1035
+ function collectServerMessageIds(items) {
1036
+ const ids = /* @__PURE__ */ new Set();
1037
+ for (const item of items) {
1038
+ if (item.kind === "server") ids.add(item.message.id);
1039
+ }
1040
+ return ids;
1041
+ }
1042
+ function latestSeenTime(previousIds, items) {
1043
+ let latest = Number.NEGATIVE_INFINITY;
1044
+ for (const item of items) {
1045
+ if (item.kind === "server" && previousIds.has(item.message.id)) latest = Math.max(latest, Date.parse(item.message.createdAt));
1046
+ }
1047
+ return latest;
1048
+ }
1049
+ function countNewIncomingMessages(previousIds, currentItems) {
1050
+ const latestSeen = latestSeenTime(previousIds, currentItems);
1051
+ return currentItems.filter(
1052
+ (item) => item.kind === "server" && item.message.direction === "outbound" && !previousIds.has(item.message.id) && Date.parse(item.message.createdAt) > latestSeen
1053
+ ).length;
1054
+ }
1055
+
1056
+ // src/participant/useStickToBottom.ts
1057
+ import { useCallback as useCallback4, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
1058
+
1059
+ // src/participant/participantScroll.ts
1060
+ var NEAR_BOTTOM_THRESHOLD_PX = 80;
1061
+ function shouldStickToBottom(params) {
1062
+ const { scrollTop, clientHeight, scrollHeight, threshold } = params;
1063
+ return scrollHeight - scrollTop - clientHeight <= threshold;
1064
+ }
1065
+ function scrollAnchorAfterPrepend(params) {
1066
+ return params.previousScrollTop + (params.scrollHeight - params.previousScrollHeight);
1067
+ }
1068
+ function resolveScrollAction(params) {
1069
+ const { firstKey, lastKey } = params;
1070
+ if (firstKey === void 0 || lastKey === void 0) return "none";
1071
+ if (!params.hasPositioned) return "bottom";
1072
+ if (lastKey !== params.previousLastKey && params.wasNearBottom) return "bottom";
1073
+ if (firstKey !== params.previousFirstKey) return "anchor";
1074
+ return "none";
1075
+ }
1076
+
1077
+ // src/participant/useStickToBottom.ts
1078
+ function useStickToBottom({ firstKey, lastKey }) {
1079
+ const ref = useRef5(null);
1080
+ const snapshot = useRef5({ hasPositioned: false, firstKey: void 0, lastKey: void 0, scrollTop: 0, scrollHeight: 0 });
1081
+ const [isNearBottom, setIsNearBottom] = useState5(true);
1082
+ const isNearBottomRef = useRef5(true);
1083
+ const onScroll = useCallback4((event) => {
1084
+ const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
1085
+ const isNear = shouldStickToBottom({ scrollTop, clientHeight, scrollHeight, threshold: NEAR_BOTTOM_THRESHOLD_PX });
1086
+ snapshot.current.scrollTop = scrollTop;
1087
+ snapshot.current.scrollHeight = scrollHeight;
1088
+ isNearBottomRef.current = isNear;
1089
+ setIsNearBottom(isNear);
1090
+ }, []);
1091
+ useLayoutEffect(() => {
1092
+ const element = ref.current;
1093
+ const previous = snapshot.current;
1094
+ if (!element) return;
1095
+ const action = resolveScrollAction({
1096
+ hasPositioned: previous.hasPositioned,
1097
+ previousFirstKey: previous.firstKey,
1098
+ previousLastKey: previous.lastKey,
1099
+ firstKey,
1100
+ lastKey,
1101
+ wasNearBottom: isNearBottomRef.current
1102
+ });
1103
+ if (action === "bottom") {
1104
+ element.scrollTop = element.scrollHeight;
1105
+ isNearBottomRef.current = true;
1106
+ setIsNearBottom(true);
1107
+ }
1108
+ if (action === "anchor") {
1109
+ element.scrollTop = scrollAnchorAfterPrepend({
1110
+ previousScrollTop: previous.scrollTop,
1111
+ previousScrollHeight: previous.scrollHeight,
1112
+ scrollHeight: element.scrollHeight
1113
+ });
1114
+ }
1115
+ snapshot.current = {
1116
+ hasPositioned: previous.hasPositioned || action === "bottom",
1117
+ firstKey,
1118
+ lastKey,
1119
+ scrollTop: element.scrollTop,
1120
+ scrollHeight: element.scrollHeight
1121
+ };
1122
+ }, [firstKey, lastKey]);
1123
+ return { scroll: { ref, onScroll }, isNearBottom };
1124
+ }
1125
+
1126
+ // src/participant/useParticipantThreadScroll.ts
1127
+ function keyOf2(item) {
1128
+ if (!item) return void 0;
1129
+ return item.kind === "server" ? item.message.id : `pending:${item.pending.clientMessageId}`;
1130
+ }
1131
+ function useParticipantThreadScroll(items) {
1132
+ const { scroll, isNearBottom } = useStickToBottom({ firstKey: keyOf2(items[0]), lastKey: keyOf2(items[items.length - 1]) });
1133
+ const seenIds = useRef6(/* @__PURE__ */ new Set());
1134
+ useEffect6(() => {
1135
+ if (isNearBottom) seenIds.current = collectServerMessageIds(items);
1136
+ }, [isNearBottom, items]);
1137
+ return { scroll, newMessagesCount: isNearBottom ? 0 : countNewIncomingMessages(seenIds.current, items) };
1138
+ }
1139
+
1140
+ // src/participant/ParticipantThreadScreen.tsx
1141
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1142
+ function useOpenedConversation(api, subject, isNeeded) {
1143
+ const [opened, setOpened] = useState6({ status: "idle" });
1144
+ const { subjectType, subjectId } = subject;
1145
+ const canOpen = api.openConversation !== void 0;
1146
+ useEffect7(() => {
1147
+ if (!isNeeded || !canOpen) return;
1148
+ let isCurrent = true;
1149
+ setOpened({ status: "loading" });
1150
+ api.openConversation?.({ subjectType, subjectId }).then(
1151
+ (conversation) => isCurrent && setOpened({ status: "found", conversation }),
1152
+ () => isCurrent && setOpened({ status: "missing" })
1153
+ );
1154
+ return () => {
1155
+ isCurrent = false;
1156
+ };
1157
+ }, [api, canOpen, isNeeded, subjectType, subjectId]);
1158
+ return opened;
1159
+ }
1160
+ function NotFound({ labels, onBack }) {
1161
+ return /* @__PURE__ */ jsxs7("div", { className: "cv-p cv-p-thread", children: [
1162
+ /* @__PURE__ */ jsx7("header", { className: "cv-p-thread__header", children: onBack ? /* @__PURE__ */ jsxs7("button", { type: "button", className: "cv-p-button cv-p-thread__back", onClick: onBack, children: [
1163
+ /* @__PURE__ */ jsx7("span", { "aria-hidden": "true", children: "\u2039" }),
1164
+ /* @__PURE__ */ jsx7("span", { className: "cv-p-sr-only", children: labels.back })
1165
+ ] }) : null }),
1166
+ /* @__PURE__ */ jsx7("p", { className: "cv-p-empty", children: labels.notFound })
1167
+ ] });
1168
+ }
1169
+ function ParticipantThreadScreen(props) {
1170
+ const { api, selected, inbox, labels } = props;
1171
+ const listed = inbox.conversations.find(
1172
+ (item) => item.subjectType === selected.subjectType && item.subjectId === selected.subjectId
1173
+ );
1174
+ const opened = useOpenedConversation(api, selected, listed === void 0);
1175
+ const conversation = listed ?? (opened.status === "found" ? opened.conversation : void 0);
1176
+ if (conversation) return /* @__PURE__ */ jsx7(LoadedThread, { ...props, conversation });
1177
+ const isResolving = opened.status === "loading" || api.openConversation !== void 0 && opened.status === "idle";
1178
+ const isListLoading = inbox.status === "idle" || inbox.status === "loading";
1179
+ if (isResolving || api.openConversation === void 0 && isListLoading) return /* @__PURE__ */ jsx7("div", { className: "cv-p cv-p-thread", "aria-busy": "true" });
1180
+ return /* @__PURE__ */ jsx7(NotFound, { labels, onBack: props.onBack });
1181
+ }
1182
+ function LoadedThread(props) {
1183
+ const { api, selected, conversation, draftsRef, inbox } = props;
1184
+ const { draft, setText, setFiles, takeForSend, restoreFromEntry } = useParticipantDraft(draftsRef, selected);
1185
+ const { thread, items, sendEntries, dispatchSend } = useParticipantConversationView({
1186
+ ...props,
1187
+ subject: selected,
1188
+ onMarkedRead: inbox.markSubjectRead
1189
+ });
1190
+ const resolveAttachmentUrl = useMemo3(() => bindAttachmentUrlResolver(api), [api]);
1191
+ const { scroll, newMessagesCount } = useParticipantThreadScroll(items);
1192
+ function handleSend() {
1193
+ const content = takeForSend();
1194
+ if (content) void thread.send(content);
1195
+ }
1196
+ function handleEdit(clientMessageId) {
1197
+ const entry = findFailedEntry(sendEntries, clientMessageId);
1198
+ if (!entry) return;
1199
+ restoreFromEntry(entry);
1200
+ dispatchSend({ type: "discarded", clientMessageId });
1201
+ }
1202
+ return /* @__PURE__ */ jsx7(
1203
+ ParticipantThread,
1204
+ {
1205
+ conversation,
1206
+ items,
1207
+ hasMore: thread.hasMore,
1208
+ labels: props.labels,
1209
+ resolveAttachmentUrl,
1210
+ draft: { value: draft.text, onChange: setText, files: draft.files, onFilesChange: setFiles },
1211
+ onSend: handleSend,
1212
+ status: thread.status,
1213
+ refresh: () => void thread.refresh(),
1214
+ scroll,
1215
+ newMessagesCount,
1216
+ isSending: hasSendingEntry(sendEntries),
1217
+ channel: props.channel,
1218
+ locale: props.locale,
1219
+ onBack: props.onBack,
1220
+ onOpenSubject: props.onOpenSubject,
1221
+ onLoadOlder: () => void thread.loadOlder(),
1222
+ pendingActions: {
1223
+ retryLocal: (clientMessageId) => void thread.retry(clientMessageId),
1224
+ discardLocal: (clientMessageId) => dispatchSend({ type: "discarded", clientMessageId }),
1225
+ editLocal: handleEdit,
1226
+ ...props.onRetryPending ? { retryHost: props.onRetryPending } : {}
1227
+ },
1228
+ quickReplies: props.quickReplies,
1229
+ renderSubjectCard: props.renderSubjectCard
1230
+ }
1231
+ );
1232
+ }
1233
+
1234
+ // src/participant/participantGrouping.ts
1235
+ var ALL_FILTER2 = "all";
1236
+ var SECTION_AWAITING = "awaiting";
1237
+ var SECTION_OTHER = "other";
1238
+ var SECTION_CLOSED = "closed";
1239
+ function compareConversations(left, right) {
1240
+ if (left.lastMessageAt !== right.lastMessageAt) {
1241
+ if (left.lastMessageAt === null) return 1;
1242
+ if (right.lastMessageAt === null) return -1;
1243
+ return left.lastMessageAt < right.lastMessageAt ? 1 : -1;
1244
+ }
1245
+ return left.subjectLabel.localeCompare(right.subjectLabel);
1246
+ }
1247
+ function sumUnread(conversations) {
1248
+ return conversations.reduce((sum, conversation) => sum + conversation.unreadCount, 0);
1249
+ }
1250
+ function resolveSectionKey(conversation, knownTypes) {
1251
+ if (conversation.status === "closed") return SECTION_CLOSED;
1252
+ if (conversation.awaitingParticipant) return SECTION_AWAITING;
1253
+ return knownTypes.has(conversation.subjectType) ? conversation.subjectType : SECTION_OTHER;
1254
+ }
1255
+ function buildSection(key, conversations) {
1256
+ const sorted = [...conversations].sort(compareConversations);
1257
+ return { key, conversations: sorted, unreadCount: sumUnread(sorted) };
1258
+ }
1259
+ function buildFilters(params) {
1260
+ const groupFilters = [];
1261
+ for (const group of params.subjectGroups) {
1262
+ const own = params.conversations.filter((conversation) => conversation.subjectType === group.subjectType);
1263
+ if (own.length === 0) continue;
1264
+ groupFilters.push({ subjectType: group.subjectType, label: group.label, unreadCount: sumUnread(own) });
1265
+ }
1266
+ const all = {
1267
+ subjectType: ALL_FILTER2,
1268
+ label: "All",
1269
+ unreadCount: sumUnread(params.conversations)
1270
+ };
1271
+ return { filters: [all, ...groupFilters], showFilters: groupFilters.length >= 2 };
1272
+ }
1273
+ function groupParticipantConversations(params) {
1274
+ const { filter, subjectGroups } = params;
1275
+ const isFiltered = filter !== void 0 && filter !== ALL_FILTER2;
1276
+ const visible = isFiltered ? params.conversations.filter((conversation) => conversation.subjectType === filter) : params.conversations;
1277
+ const knownTypes = new Set(subjectGroups.map((group) => group.subjectType));
1278
+ const buckets = /* @__PURE__ */ new Map();
1279
+ for (const conversation of visible) {
1280
+ const key = resolveSectionKey(conversation, knownTypes);
1281
+ buckets.set(key, [...buckets.get(key) ?? [], conversation]);
1282
+ }
1283
+ const orderedKeys = [SECTION_AWAITING, ...subjectGroups.map((group) => group.subjectType), SECTION_OTHER, SECTION_CLOSED];
1284
+ const sections = orderedKeys.flatMap((key) => {
1285
+ const bucket = buckets.get(key);
1286
+ return bucket === void 0 ? [] : [buildSection(key, bucket)];
1287
+ });
1288
+ return { sections, ...buildFilters(params) };
1289
+ }
1290
+
1291
+ // src/participant/participantTheme.ts
1292
+ var THEME_TO_VARIABLE = {
1293
+ primaryColor: "--cv-p-accent",
1294
+ backgroundColor: "--cv-p-surface",
1295
+ textPrimary: "--cv-p-text",
1296
+ textSecondary: "--cv-p-text-muted"
1297
+ };
1298
+ function buildParticipantThemeStyle(theme) {
1299
+ if (!theme) return void 0;
1300
+ const style = {};
1301
+ for (const [key, variable] of Object.entries(THEME_TO_VARIABLE)) {
1302
+ const value = theme[key];
1303
+ if (value) style[variable] = value;
1304
+ }
1305
+ return Object.keys(style).length > 0 ? style : void 0;
1306
+ }
1307
+
1308
+ // src/participant/ParticipantConversationsScreen.tsx
1309
+ import { jsx as jsx8 } from "react/jsx-runtime";
1310
+ var ALL_FILTER3 = "all";
1311
+ function ParticipantConversationsScreen(props) {
1312
+ const { selected, inbox, subjectGroups, classNames } = props;
1313
+ const [filter, setFilter] = useState7(ALL_FILTER3);
1314
+ const draftsRef = useRef7(/* @__PURE__ */ new Map());
1315
+ const [sendStates, dispatchSendStates] = useReducer2(participantSendStatesReducer, /* @__PURE__ */ new Map());
1316
+ const labels = useMemo4(
1317
+ () => ({ ...DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS, ...props.labels }),
1318
+ [props.labels]
1319
+ );
1320
+ const view = useMemo4(
1321
+ () => groupParticipantConversations({ conversations: inbox.conversations, subjectGroups, filter }),
1322
+ [inbox.conversations, subjectGroups, filter]
1323
+ );
1324
+ const rootClassName = ["cv-p-root", props.className, classNames?.root].filter(Boolean).join(" ");
1325
+ return /* @__PURE__ */ jsx8("div", { className: rootClassName, style: buildParticipantThemeStyle(props.theme), children: selected === void 0 ? /* @__PURE__ */ jsx8("div", { className: ["cv-p-root__screen", classNames?.inbox].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx8(
1326
+ ParticipantInbox,
1327
+ {
1328
+ view,
1329
+ subjectGroups,
1330
+ filter,
1331
+ onFilterChange: setFilter,
1332
+ onSelect: props.onSelect,
1333
+ labels,
1334
+ status: inbox.status,
1335
+ refresh: () => void inbox.refresh(),
1336
+ hasMore: inbox.hasMore,
1337
+ loadMore: () => void inbox.loadMore(),
1338
+ locale: props.locale
1339
+ }
1340
+ ) }) : /* @__PURE__ */ jsx8("div", { className: ["cv-p-root__screen", classNames?.thread].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx8(
1341
+ ParticipantThreadScreen,
1342
+ {
1343
+ ...props,
1344
+ selected,
1345
+ labels,
1346
+ draftsRef,
1347
+ sendStates,
1348
+ dispatchSendStates
1349
+ },
1350
+ draftKey(selected)
1351
+ ) }) });
1352
+ }
1353
+
1354
+ // src/participant/useParticipantInbox.ts
1355
+ import { useCallback as useCallback5, useEffect as useEffect8, useReducer as useReducer3 } from "react";
1356
+
1357
+ // src/participant/participantInboxState.ts
1358
+ var INITIAL_PARTICIPANT_INBOX_STATE = { status: "idle", conversations: [] };
1359
+ function subjectKey(subject) {
1360
+ return `${subject.subjectType}:${subject.subjectId}`;
1361
+ }
1362
+ function withCursor(state, conversations, nextCursor) {
1363
+ const { error: _error, nextCursor: _nextCursor, ...rest } = state;
1364
+ return { ...rest, status: "ready", conversations, ...nextCursor === void 0 ? {} : { nextCursor } };
1365
+ }
1366
+ function mergeFirstPage(state, incoming, nextCursor) {
1367
+ const incomingKeys = new Set(incoming.map(subjectKey));
1368
+ const retained = state.conversations.filter((conversation) => !incomingKeys.has(subjectKey(conversation)));
1369
+ const cursor = retained.length > 0 ? state.nextCursor : nextCursor;
1370
+ return withCursor(state, [...incoming, ...retained], cursor);
1371
+ }
1372
+ function appendUnique(current, incoming) {
1373
+ const incomingByKey = new Map(incoming.map((conversation) => [subjectKey(conversation), conversation]));
1374
+ const updated = current.map((conversation) => incomingByKey.get(subjectKey(conversation)) ?? conversation);
1375
+ const knownKeys = new Set(current.map(subjectKey));
1376
+ return [...updated, ...incoming.filter((conversation) => !knownKeys.has(subjectKey(conversation)))];
1377
+ }
1378
+ function participantInboxReducer(state, action) {
1379
+ switch (action.type) {
1380
+ case "started":
1381
+ return state.status === "ready" ? state : { ...state, status: "loading" };
1382
+ case "loaded":
1383
+ return mergeFirstPage(state, action.conversations, action.nextCursor);
1384
+ case "appended":
1385
+ return withCursor(state, appendUnique(state.conversations, action.conversations), action.nextCursor);
1386
+ case "markedRead": {
1387
+ const target = subjectKey(action.subject);
1388
+ return {
1389
+ ...state,
1390
+ conversations: state.conversations.map(
1391
+ (conversation) => subjectKey(conversation) === target ? { ...conversation, unreadCount: 0 } : conversation
1392
+ )
1393
+ };
1394
+ }
1395
+ case "failed":
1396
+ return { ...state, status: "error", error: action.error };
1397
+ }
1398
+ }
1399
+
1400
+ // src/participant/useParticipantInbox.ts
1401
+ function describeError2(error) {
1402
+ return error instanceof Error ? error.message : "unknown error";
1403
+ }
1404
+ function useParticipantInbox(api) {
1405
+ const [state, dispatch] = useReducer3(participantInboxReducer, INITIAL_PARTICIPANT_INBOX_STATE);
1406
+ const refresh = useCallback5(async () => {
1407
+ dispatch({ type: "started" });
1408
+ try {
1409
+ const page = await api.listConversations();
1410
+ dispatch({ type: "loaded", conversations: page.data, ...page.nextCursor ? { nextCursor: page.nextCursor } : {} });
1411
+ } catch (error) {
1412
+ dispatch({ type: "failed", error: describeError2(error) });
1413
+ }
1414
+ }, [api]);
1415
+ const { nextCursor } = state;
1416
+ const loadMore = useCallback5(async () => {
1417
+ if (!nextCursor) return;
1418
+ try {
1419
+ const page = await api.listConversations({ cursor: nextCursor });
1420
+ dispatch({ type: "appended", conversations: page.data, ...page.nextCursor ? { nextCursor: page.nextCursor } : {} });
1421
+ } catch (error) {
1422
+ dispatch({ type: "failed", error: describeError2(error) });
1423
+ }
1424
+ }, [api, nextCursor]);
1425
+ const markSubjectRead = useCallback5((subject) => {
1426
+ dispatch({ type: "markedRead", subject });
1427
+ }, []);
1428
+ useEffect8(() => {
1429
+ void refresh();
1430
+ }, [refresh]);
1431
+ useParticipantRevalidation(api, () => {
1432
+ void refresh();
1433
+ });
1434
+ return { ...state, hasMore: nextCursor !== void 0, refresh, loadMore, markSubjectRead };
1435
+ }
1436
+
1437
+ // src/participant/ParticipantConversations.tsx
1438
+ import { jsx as jsx9 } from "react/jsx-runtime";
1439
+ function ParticipantConversations(props) {
1440
+ const inbox = useParticipantInbox(props.api);
1441
+ const { status, conversations, hasMore, refresh, loadMore, markSubjectRead } = inbox;
1442
+ return /* @__PURE__ */ jsx9(ParticipantConversationsScreen, { ...props, inbox: { status, conversations, hasMore, refresh, loadMore, markSubjectRead } });
1443
+ }
1444
+ export {
1445
+ DEFAULT_PARTICIPANT_CONVERSATIONS_LABELS,
1446
+ ParticipantConversations,
1447
+ groupParticipantConversations
1448
+ };