@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.
- package/README.md +169 -0
- package/dist/{ConversationSimulatorPanel-DnXElfY8.d.ts → ConversationSimulatorPanel-CrfFjiWh.d.ts} +2 -268
- package/dist/{chunk-PJNR6UMN.js → chunk-CBYEHPX6.js} +1 -63
- package/dist/{chunk-HMCOTZAX.js → chunk-HOKNKXIT.js} +1 -1
- package/dist/{chunk-4P5KMWOI.js → chunk-JXJCPN34.js} +327 -522
- package/dist/chunk-MRVFFJEH.js +64 -0
- package/dist/chunk-RJMSM3YA.js +249 -0
- package/dist/chunk-WSFGNR5R.js +55 -0
- package/dist/flows/index.js +5 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +783 -807
- package/dist/participant/index.d.ts +162 -0
- package/dist/participant/index.js +1448 -0
- package/dist/preview/index.d.ts +4 -2
- package/dist/preview/index.js +4 -2
- package/dist/quickReply.types-B4gmZsSr.d.ts +274 -0
- package/dist/styles.css +591 -0
- package/package.json +8 -4
- package/src/ChannelIcon.tsx +2 -0
- package/src/MessageBubble.test.tsx +2 -2
- package/src/MessageText.test.tsx +59 -0
- package/src/MessageText.tsx +20 -12
- package/src/MessageTextCopiedBadge.tsx +7 -0
- package/src/StatusTicks.test.tsx +30 -0
- package/src/StatusTicks.tsx +24 -9
- package/src/buildOutput.test.ts +41 -0
- package/src/channelCapability.test.ts +17 -0
- package/src/conversationChannel.test.ts +30 -1
- package/src/conversationChannel.ts +19 -1
- package/src/participant/ParticipantAttachmentItem.tsx +77 -0
- package/src/participant/ParticipantComposer.test.tsx +97 -0
- package/src/participant/ParticipantComposer.tsx +167 -0
- package/src/participant/ParticipantConversations.test.tsx +90 -0
- package/src/participant/ParticipantConversations.tsx +42 -0
- package/src/participant/ParticipantConversationsScreen.tsx +67 -0
- package/src/participant/ParticipantInbox.test.tsx +147 -0
- package/src/participant/ParticipantInbox.tsx +168 -0
- package/src/participant/ParticipantLoadState.tsx +27 -0
- package/src/participant/ParticipantMessageBubble.test.tsx +165 -0
- package/src/participant/ParticipantMessageBubble.tsx +185 -0
- package/src/participant/ParticipantThread.test.tsx +132 -0
- package/src/participant/ParticipantThread.tsx +174 -0
- package/src/participant/ParticipantThreadScreen.tsx +135 -0
- package/src/participant/bindAttachmentUrlResolver.test.ts +25 -0
- package/src/participant/bindAttachmentUrlResolver.ts +7 -0
- package/src/participant/index.ts +25 -0
- package/src/participant/participant.types.ts +43 -0
- package/src/participant/participantApi.types.ts +59 -0
- package/src/participant/participantAttachmentUrl.test.ts +32 -0
- package/src/participant/participantAttachmentUrl.ts +10 -0
- package/src/participant/participantBubbleActions.test.ts +44 -0
- package/src/participant/participantBubbleActions.ts +40 -0
- package/src/participant/participantDrafts.test.ts +65 -0
- package/src/participant/participantDrafts.ts +50 -0
- package/src/participant/participantFileValidation.test.ts +86 -0
- package/src/participant/participantFileValidation.ts +72 -0
- package/src/participant/participantFixtures.test-helper.ts +41 -0
- package/src/participant/participantGrouping.test.ts +138 -0
- package/src/participant/participantGrouping.ts +77 -0
- package/src/participant/participantInboxState.test.ts +69 -0
- package/src/participant/participantInboxState.ts +74 -0
- package/src/participant/participantLabels.ts +86 -0
- package/src/participant/participantLoadView.test.ts +23 -0
- package/src/participant/participantLoadView.ts +21 -0
- package/src/participant/participantMessages.test.ts +128 -0
- package/src/participant/participantMessages.ts +73 -0
- package/src/participant/participantNewMessages.test.ts +54 -0
- package/src/participant/participantNewMessages.ts +29 -0
- package/src/participant/participantQuickReply.test.ts +23 -0
- package/src/participant/participantQuickReply.ts +6 -0
- package/src/participant/participantScroll.test.ts +69 -0
- package/src/participant/participantScroll.ts +43 -0
- package/src/participant/participantSendController.test.ts +201 -0
- package/src/participant/participantSendController.ts +134 -0
- package/src/participant/participantSendStates.ts +31 -0
- package/src/participant/participantTheme.ts +20 -0
- package/src/participant/participantThreadState.test.ts +75 -0
- package/src/participant/participantThreadState.ts +59 -0
- package/src/participant/shouldMarkRead.test.ts +24 -0
- package/src/participant/shouldMarkRead.ts +16 -0
- package/src/participant/useDocumentVisibility.ts +18 -0
- package/src/participant/useParticipantConversationView.ts +60 -0
- package/src/participant/useParticipantDraft.ts +45 -0
- package/src/participant/useParticipantInbox.ts +57 -0
- package/src/participant/useParticipantMarkRead.ts +26 -0
- package/src/participant/useParticipantRevalidation.ts +36 -0
- package/src/participant/useParticipantSend.ts +84 -0
- package/src/participant/useParticipantThread.ts +116 -0
- package/src/participant/useParticipantThreadScroll.ts +26 -0
- package/src/participant/useStickToBottom.ts +77 -0
- package/src/preview/ConversationSimulatorPanel.tsx +8 -0
- package/src/styles.css +160 -0
- 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
|
+
};
|