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