@mailoo/chat 0.0.0-stage → 1.32.2
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/LICENSE +16 -0
- package/README.md +150 -2
- package/dist/chat-message-Dw6tKBEb.d.cts +10 -0
- package/dist/chat-message-Dw6tKBEb.d.ts +10 -0
- package/dist/chunk-2Y37RYEL.js +141 -0
- package/dist/chunk-2Y37RYEL.js.map +1 -0
- package/dist/chunk-4TNHJERE.js +19 -0
- package/dist/chunk-4TNHJERE.js.map +1 -0
- package/dist/chunk-77E6HUHY.js +41 -0
- package/dist/chunk-77E6HUHY.js.map +1 -0
- package/dist/chunk-AU6EFFLV.js +431 -0
- package/dist/chunk-AU6EFFLV.js.map +1 -0
- package/dist/chunk-QZ7HWQDO.js +839 -0
- package/dist/chunk-QZ7HWQDO.js.map +1 -0
- package/dist/client/index.cjs +1658 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +33 -0
- package/dist/client/index.d.ts +33 -0
- package/dist/client/index.js +597 -0
- package/dist/client/index.js.map +1 -0
- package/dist/hooks/index.cjs +751 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +79 -0
- package/dist/hooks/index.d.ts +79 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.cjs +213 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/mailoo-site-chat-widget-C7B797Lu.d.ts +130 -0
- package/dist/mailoo-site-chat-widget-h_cBqsJw.d.cts +130 -0
- package/dist/routes/index.cjs +204 -0
- package/dist/routes/index.cjs.map +1 -0
- package/dist/routes/index.d.cts +30 -0
- package/dist/routes/index.d.ts +30 -0
- package/dist/routes/index.js +174 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/shared-glVmghqk.d.cts +40 -0
- package/dist/shared-glVmghqk.d.ts +40 -0
- package/dist/styles/widget.css +122 -0
- package/dist/welcome-presets/index.cjs +479 -0
- package/dist/welcome-presets/index.cjs.map +1 -0
- package/dist/welcome-presets/index.d.cts +53 -0
- package/dist/welcome-presets/index.d.ts +53 -0
- package/dist/welcome-presets/index.js +38 -0
- package/dist/welcome-presets/index.js.map +1 -0
- package/dist/welcome-presets/parse.cjs +44 -0
- package/dist/welcome-presets/parse.cjs.map +1 -0
- package/dist/welcome-presets/parse.d.cts +11 -0
- package/dist/welcome-presets/parse.d.ts +11 -0
- package/dist/welcome-presets/parse.js +9 -0
- package/dist/welcome-presets/parse.js.map +1 -0
- package/dist/widget-config-B5Tf7gqn.d.cts +32 -0
- package/dist/widget-config-B5Tf7gqn.d.ts +32 -0
- package/package.json +85 -3
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
// src/hooks/index.ts
|
|
2
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/lib/chat-message.ts
|
|
5
|
+
function parseChatMessages(rows) {
|
|
6
|
+
const parsed = [];
|
|
7
|
+
for (const row of rows) {
|
|
8
|
+
if (!row || typeof row !== "object") continue;
|
|
9
|
+
const r = row;
|
|
10
|
+
if (typeof r.id !== "string" || typeof r.content !== "string") continue;
|
|
11
|
+
parsed.push({
|
|
12
|
+
id: r.id,
|
|
13
|
+
messageType: typeof r.messageType === "string" ? r.messageType : "INBOUND",
|
|
14
|
+
content: r.content,
|
|
15
|
+
senderEmail: typeof r.senderEmail === "string" ? r.senderEmail : null,
|
|
16
|
+
senderName: typeof r.senderName === "string" ? r.senderName : null,
|
|
17
|
+
createdAt: typeof r.createdAt === "string" ? r.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
function messagesEqual(a, b) {
|
|
23
|
+
if (a.length !== b.length) return false;
|
|
24
|
+
for (let i = 0; i < a.length; i++) {
|
|
25
|
+
const x = a[i];
|
|
26
|
+
const y = b[i];
|
|
27
|
+
if (x.id !== y.id || x.content !== y.content || x.messageType !== y.messageType || x.createdAt !== y.createdAt || x.senderEmail !== y.senderEmail || x.senderName !== y.senderName) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
function mergeChatMessages(prev, incoming) {
|
|
34
|
+
if (incoming.length === 0) return prev;
|
|
35
|
+
const byId = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const m of prev) byId.set(m.id, m);
|
|
37
|
+
for (const m of incoming) byId.set(m.id, m);
|
|
38
|
+
const next = Array.from(byId.values()).sort(
|
|
39
|
+
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
40
|
+
);
|
|
41
|
+
return messagesEqual(prev, next) ? prev : next;
|
|
42
|
+
}
|
|
43
|
+
function latestMessageCreatedAt(messages) {
|
|
44
|
+
if (messages.length === 0) return void 0;
|
|
45
|
+
let latest = messages[0].createdAt;
|
|
46
|
+
for (const m of messages) {
|
|
47
|
+
if (new Date(m.createdAt).getTime() > new Date(latest).getTime()) {
|
|
48
|
+
latest = m.createdAt;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return latest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/hooks/shared.ts
|
|
55
|
+
var DEFAULT_CHAT_SUBMIT_ENDPOINT = "/api/v1/webhooks/chat/submit";
|
|
56
|
+
var DEFAULT_CHAT_MESSAGES_ENDPOINT = "/api/v1/webhooks/chat/messages";
|
|
57
|
+
var DEFAULT_CHAT_STATUS_ENDPOINT = "/api/v1/webhooks/chat/status";
|
|
58
|
+
var DEFAULT_CHAT_SESSION_STORAGE_KEY = "mailoo_site_chat_session_public_id";
|
|
59
|
+
var DEFAULT_CHAT_GUEST_PROFILE_KEY = "mailoo_site_chat_guest_profile";
|
|
60
|
+
var DEFAULT_CHAT_POLL_INTERVAL_MS = 5e3;
|
|
61
|
+
function messageFromPayload(data, fallback) {
|
|
62
|
+
if (!data || typeof data !== "object") return fallback;
|
|
63
|
+
const d = data;
|
|
64
|
+
if (typeof d.message === "string") return d.message;
|
|
65
|
+
if (typeof d.error === "string") return d.error;
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
function classifyChatFault(status, payload, fallbackMessage = "Request failed") {
|
|
69
|
+
if (status === 429) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "rate_limited",
|
|
72
|
+
status,
|
|
73
|
+
message: messageFromPayload(payload, fallbackMessage)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (status === 0 || status === 502 || status === 503 || status >= 500) {
|
|
77
|
+
return {
|
|
78
|
+
kind: "unavailable",
|
|
79
|
+
status,
|
|
80
|
+
message: messageFromPayload(payload, fallbackMessage)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function isChatSessionNotFound(status, data) {
|
|
86
|
+
if (status !== 404) return false;
|
|
87
|
+
const text = messageFromPayload(data, "").toLowerCase();
|
|
88
|
+
return text.includes("chat session not found");
|
|
89
|
+
}
|
|
90
|
+
function persistSessionPublicId(id, storageKey, ctx) {
|
|
91
|
+
const created = !ctx.sessionPublicIdRef.current;
|
|
92
|
+
ctx.setSessionPublicId(id);
|
|
93
|
+
ctx.sessionPublicIdRef.current = id;
|
|
94
|
+
try {
|
|
95
|
+
sessionStorage.setItem(storageKey, id);
|
|
96
|
+
} catch {
|
|
97
|
+
}
|
|
98
|
+
if (created) ctx.onCreated?.(id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/hooks/use-mailoo-chat-session.ts
|
|
102
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
103
|
+
|
|
104
|
+
// src/lib/guest-copy.ts
|
|
105
|
+
function isValidEmail(s) {
|
|
106
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s.trim());
|
|
107
|
+
}
|
|
108
|
+
function unavailableCopy(locale) {
|
|
109
|
+
switch (locale) {
|
|
110
|
+
case "ru":
|
|
111
|
+
return {
|
|
112
|
+
rateLimitedTitle: "\u0427\u0430\u0442 \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
113
|
+
rateLimitedBody: "\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432. \u041F\u043E\u0434\u043E\u0436\u0434\u0438\u0442\u0435 \u043D\u0435\u043C\u043D\u043E\u0433\u043E \u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0441\u043D\u043E\u0432\u0430 \u0438\u043B\u0438 \u0441\u0432\u044F\u0436\u0438\u0442\u0435\u0441\u044C \u0441 \u043D\u0430\u043C\u0438 \u0434\u0440\u0443\u0433\u0438\u043C \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C.",
|
|
114
|
+
unavailableTitle: "\u0427\u0430\u0442 \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
115
|
+
unavailableBody: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0432\u044F\u0437\u0430\u0442\u044C\u0441\u044F \u0441 \u0447\u0430\u0442\u043E\u043C. \u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u043D\u0430\u043C \u0434\u0440\u0443\u0433\u0438\u043C \u0441\u043F\u043E\u0441\u043E\u0431\u043E\u043C.",
|
|
116
|
+
retryCta: "\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C",
|
|
117
|
+
contactCta: "\u0421\u0432\u044F\u0437\u0430\u0442\u044C\u0441\u044F \u0441 \u043D\u0430\u043C\u0438"
|
|
118
|
+
};
|
|
119
|
+
case "de":
|
|
120
|
+
return {
|
|
121
|
+
rateLimitedTitle: "Chat vor\xFCbergehend nicht verf\xFCgbar",
|
|
122
|
+
rateLimitedBody: "Zu viele Anfragen. Bitte warten Sie kurz und versuchen Sie es erneut, oder kontaktieren Sie uns auf anderem Wege.",
|
|
123
|
+
unavailableTitle: "Chat vor\xFCbergehend nicht verf\xFCgbar",
|
|
124
|
+
unavailableBody: "Der Chat ist gerade nicht erreichbar. Bitte versuchen Sie es erneut oder kontaktieren Sie uns auf anderem Wege.",
|
|
125
|
+
retryCta: "Erneut versuchen",
|
|
126
|
+
contactCta: "Kontakt aufnehmen"
|
|
127
|
+
};
|
|
128
|
+
case "es":
|
|
129
|
+
return {
|
|
130
|
+
rateLimitedTitle: "Chat temporalmente no disponible",
|
|
131
|
+
rateLimitedBody: "Hay demasiadas solicitudes. Espera un momento e int\xE9ntalo de nuevo, o cont\xE1ctanos de otra forma.",
|
|
132
|
+
unavailableTitle: "Chat temporalmente no disponible",
|
|
133
|
+
unavailableBody: "No pudimos conectar con el chat. Int\xE9ntalo de nuevo o cont\xE1ctanos de otra forma.",
|
|
134
|
+
retryCta: "Reintentar",
|
|
135
|
+
contactCta: "Contactarnos"
|
|
136
|
+
};
|
|
137
|
+
default:
|
|
138
|
+
return {
|
|
139
|
+
rateLimitedTitle: "Chat temporarily unavailable",
|
|
140
|
+
rateLimitedBody: "Too many requests right now. Please wait a moment and try again, or reach us another way.",
|
|
141
|
+
unavailableTitle: "Chat temporarily unavailable",
|
|
142
|
+
unavailableBody: "We could not reach chat right now. Please try again, or contact us another way.",
|
|
143
|
+
retryCta: "Try again",
|
|
144
|
+
contactCta: "Contact us"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function guestCopy(locale) {
|
|
149
|
+
switch (locale) {
|
|
150
|
+
case "ru":
|
|
151
|
+
return {
|
|
152
|
+
intro: "\u0427\u0442\u043E\u0431\u044B \u043C\u044B \u043C\u043E\u0433\u043B\u0438 \u0441 \u0432\u0430\u043C\u0438 \u0441\u0432\u044F\u0437\u0430\u0442\u044C\u0441\u044F, \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B (\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E) \u0438 \u043F\u0440\u0438 \u0436\u0435\u043B\u0430\u043D\u0438\u0438 \u2014 \u0438\u043C\u044F.",
|
|
153
|
+
emailLabel: "\u042D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0430\u044F \u043F\u043E\u0447\u0442\u0430",
|
|
154
|
+
nameLabel: "\u0418\u043C\u044F",
|
|
155
|
+
emailPlaceholder: "you@example.com",
|
|
156
|
+
namePlaceholder: "\u041D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E",
|
|
157
|
+
continueCta: "\u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
|
|
158
|
+
changeContact: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u043E\u043D\u0442\u0430\u043A\u0442\u044B",
|
|
159
|
+
backToTopics: "\u041A \u0441\u043F\u0438\u0441\u043A\u0443 \u0442\u0435\u043C",
|
|
160
|
+
emailInvalid: "\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B.",
|
|
161
|
+
threadHint: "\u041F\u0435\u0440\u0435\u043F\u0438\u0441\u043A\u0430 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043D\u0438\u0436\u0435. \u041C\u044B \u043F\u0440\u043E\u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0435\u043C \u043F\u0435\u0440\u0435\u043F\u0438\u0441\u043A\u0443 \u043D\u0430 \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0439 email \u0432 \u0441\u043B\u0443\u0447\u0430\u0435 \u043E\u0431\u0440\u044B\u0432\u0430 \u0441\u0432\u044F\u0437\u0438.",
|
|
162
|
+
threadHintIconLabel: "\u041F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430: \u043F\u0435\u0440\u0435\u043F\u0438\u0441\u043A\u0430 \u0438 \u0434\u0443\u0431\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043D\u0430 email",
|
|
163
|
+
threadHintCollapse: "\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C",
|
|
164
|
+
emptyThreadGuest: "\u041D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u043F\u0435\u0440\u0432\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432 \u043F\u043E\u043B\u0435 \u043D\u0438\u0436\u0435.",
|
|
165
|
+
loadingProfile: "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026"
|
|
166
|
+
};
|
|
167
|
+
case "de":
|
|
168
|
+
return {
|
|
169
|
+
intro: "Damit wir antworten k\xF6nnen, geben Sie bitte Ihre E-Mail-Adresse an (Pflichtfeld) und optional Ihren Namen.",
|
|
170
|
+
emailLabel: "E-Mail",
|
|
171
|
+
nameLabel: "Name",
|
|
172
|
+
emailPlaceholder: "you@example.com",
|
|
173
|
+
namePlaceholder: "Optional",
|
|
174
|
+
continueCta: "Weiter",
|
|
175
|
+
changeContact: "Kontakt \xE4ndern",
|
|
176
|
+
backToTopics: "Zur\xFCck zu den Themen",
|
|
177
|
+
emailInvalid: "Bitte geben Sie eine g\xFCltige E-Mail-Adresse ein.",
|
|
178
|
+
threadHint: "Der Chatverlauf erscheint unten. Wir antworten per E-Mail.",
|
|
179
|
+
threadHintIconLabel: "Hinweis zu Chat und E-Mail",
|
|
180
|
+
threadHintCollapse: "Einklappen",
|
|
181
|
+
emptyThreadGuest: "Schreiben Sie Ihre erste Nachricht unten.",
|
|
182
|
+
loadingProfile: "Wird geladen\u2026"
|
|
183
|
+
};
|
|
184
|
+
case "es":
|
|
185
|
+
return {
|
|
186
|
+
intro: "Para poder responderte, indica tu correo electr\xF3nico (obligatorio) y, si quieres, tu nombre.",
|
|
187
|
+
emailLabel: "Correo electr\xF3nico",
|
|
188
|
+
nameLabel: "Nombre",
|
|
189
|
+
emailPlaceholder: "tu@ejemplo.com",
|
|
190
|
+
namePlaceholder: "Opcional",
|
|
191
|
+
continueCta: "Continuar",
|
|
192
|
+
changeContact: "Cambiar datos",
|
|
193
|
+
backToTopics: "Volver a los temas",
|
|
194
|
+
emailInvalid: "Introduce un correo electr\xF3nico v\xE1lido.",
|
|
195
|
+
threadHint: "Los mensajes aparecen abajo. Te responderemos por correo.",
|
|
196
|
+
threadHintIconLabel: "Aviso sobre el chat y el correo",
|
|
197
|
+
threadHintCollapse: "Ocultar",
|
|
198
|
+
emptyThreadGuest: "Escribe tu primer mensaje abajo.",
|
|
199
|
+
loadingProfile: "Cargando\u2026"
|
|
200
|
+
};
|
|
201
|
+
default:
|
|
202
|
+
return {
|
|
203
|
+
intro: "So we can reach you, please enter your email address (required) and your name if you\u2019d like (optional).",
|
|
204
|
+
emailLabel: "Email",
|
|
205
|
+
nameLabel: "Name",
|
|
206
|
+
emailPlaceholder: "you@example.com",
|
|
207
|
+
namePlaceholder: "Optional",
|
|
208
|
+
continueCta: "Continue",
|
|
209
|
+
changeContact: "Change contact details",
|
|
210
|
+
backToTopics: "Back to topics",
|
|
211
|
+
emailInvalid: "Please enter a valid email address.",
|
|
212
|
+
threadHint: "Your conversation appears below. We\u2019ll reply to your email.",
|
|
213
|
+
threadHintIconLabel: "Note about chat and email",
|
|
214
|
+
threadHintCollapse: "Collapse",
|
|
215
|
+
emptyThreadGuest: "Write your first message in the box below.",
|
|
216
|
+
loadingProfile: "Loading\u2026"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function defaultChatUiLabels(locale) {
|
|
221
|
+
return {
|
|
222
|
+
...guestCopy(locale),
|
|
223
|
+
...unavailableCopy(locale),
|
|
224
|
+
title: "Chat",
|
|
225
|
+
launcherOpen: "Open chat",
|
|
226
|
+
launcherClose: "Close chat",
|
|
227
|
+
send: "Send",
|
|
228
|
+
composerPlaceholder: "Type a message\u2026",
|
|
229
|
+
emptyThreadAuthenticated: "Say hello to start a conversation."
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/lib/visitor-ws-url.ts
|
|
234
|
+
function buildVisitorWsUrl(apiOrigin, projectUid, integrationId, sessionPublicId) {
|
|
235
|
+
const u = new URL(apiOrigin);
|
|
236
|
+
const wsProto = u.protocol === "https:" ? "wss:" : "ws:";
|
|
237
|
+
const qs = new URLSearchParams({
|
|
238
|
+
projectUid,
|
|
239
|
+
integrationId,
|
|
240
|
+
sessionPublicId
|
|
241
|
+
});
|
|
242
|
+
return `${wsProto}//${u.host}/api/v1/chat/visitor-ws?${qs.toString()}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/hooks/use-mailoo-chat-session.ts
|
|
246
|
+
function useMailooChatSession(options) {
|
|
247
|
+
const {
|
|
248
|
+
apiOrigin,
|
|
249
|
+
projectUid,
|
|
250
|
+
integrationId,
|
|
251
|
+
isAuthenticated,
|
|
252
|
+
submitEndpoint = DEFAULT_CHAT_SUBMIT_ENDPOINT,
|
|
253
|
+
messagesEndpoint = DEFAULT_CHAT_MESSAGES_ENDPOINT,
|
|
254
|
+
storageKey = DEFAULT_CHAT_SESSION_STORAGE_KEY,
|
|
255
|
+
guestProfileKey = DEFAULT_CHAT_GUEST_PROFILE_KEY,
|
|
256
|
+
pollIntervalMs = DEFAULT_CHAT_POLL_INTERVAL_MS,
|
|
257
|
+
enableRealtime = true,
|
|
258
|
+
enablePolling = true,
|
|
259
|
+
enableWebSocket = true,
|
|
260
|
+
enabled = true,
|
|
261
|
+
onSessionCreated,
|
|
262
|
+
onMessageSent,
|
|
263
|
+
onError,
|
|
264
|
+
onFault
|
|
265
|
+
} = options;
|
|
266
|
+
const [sessionPublicId, setSessionPublicId] = useState(null);
|
|
267
|
+
const [messages, setMessages] = useState([]);
|
|
268
|
+
const [sending, setSending] = useState(false);
|
|
269
|
+
const [error, setError] = useState(null);
|
|
270
|
+
const [fault, setFault] = useState(null);
|
|
271
|
+
const [connectionState, setConnectionState] = useState("idle");
|
|
272
|
+
const [guestEmail, setGuestEmail] = useState("");
|
|
273
|
+
const [guestName, setGuestName] = useState("");
|
|
274
|
+
const [guestContactOk, setGuestContactOk] = useState(isAuthenticated);
|
|
275
|
+
const [guestProfileHydrated, setGuestProfileHydrated] = useState(false);
|
|
276
|
+
const wsRef = useRef(null);
|
|
277
|
+
const pollRef = useRef(null);
|
|
278
|
+
const sessionPublicIdRef = useRef(null);
|
|
279
|
+
const sinceRef = useRef(void 0);
|
|
280
|
+
const faultRef = useRef(null);
|
|
281
|
+
const onSessionCreatedRef = useRef(onSessionCreated);
|
|
282
|
+
const onMessageSentRef = useRef(onMessageSent);
|
|
283
|
+
const onErrorRef = useRef(onError);
|
|
284
|
+
const onFaultRef = useRef(onFault);
|
|
285
|
+
sessionPublicIdRef.current = sessionPublicId;
|
|
286
|
+
faultRef.current = fault;
|
|
287
|
+
onSessionCreatedRef.current = onSessionCreated;
|
|
288
|
+
onMessageSentRef.current = onMessageSent;
|
|
289
|
+
onErrorRef.current = onError;
|
|
290
|
+
onFaultRef.current = onFault;
|
|
291
|
+
const clearFault = useCallback(() => {
|
|
292
|
+
faultRef.current = null;
|
|
293
|
+
setFault(null);
|
|
294
|
+
}, []);
|
|
295
|
+
const reportFault = useCallback((next) => {
|
|
296
|
+
faultRef.current = next;
|
|
297
|
+
setFault(next);
|
|
298
|
+
setError(null);
|
|
299
|
+
onFaultRef.current?.(next);
|
|
300
|
+
onErrorRef.current?.(next.message);
|
|
301
|
+
}, []);
|
|
302
|
+
const reportError = useCallback((message) => {
|
|
303
|
+
setError(message);
|
|
304
|
+
if (message) {
|
|
305
|
+
clearFault();
|
|
306
|
+
onErrorRef.current?.(message);
|
|
307
|
+
}
|
|
308
|
+
}, [clearFault]);
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (typeof window === "undefined") return;
|
|
311
|
+
const existing = sessionStorage.getItem(storageKey);
|
|
312
|
+
if (existing) setSessionPublicId(existing);
|
|
313
|
+
}, [storageKey]);
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (typeof window === "undefined" || isAuthenticated) {
|
|
316
|
+
setGuestProfileHydrated(true);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const raw = sessionStorage.getItem(guestProfileKey);
|
|
321
|
+
if (raw) {
|
|
322
|
+
const j = JSON.parse(raw);
|
|
323
|
+
const em = typeof j.email === "string" ? j.email.trim().toLowerCase() : "";
|
|
324
|
+
if (em && isValidEmail(em)) {
|
|
325
|
+
setGuestEmail(em);
|
|
326
|
+
setGuestName(typeof j.name === "string" ? j.name.trim() : "");
|
|
327
|
+
setGuestContactOk(true);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
setGuestProfileHydrated(true);
|
|
333
|
+
}, [isAuthenticated, guestProfileKey]);
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (isAuthenticated) {
|
|
336
|
+
setGuestContactOk(true);
|
|
337
|
+
}
|
|
338
|
+
}, [isAuthenticated]);
|
|
339
|
+
const persistGuestProfile = useCallback(
|
|
340
|
+
(email, name) => {
|
|
341
|
+
try {
|
|
342
|
+
sessionStorage.setItem(
|
|
343
|
+
guestProfileKey,
|
|
344
|
+
JSON.stringify({
|
|
345
|
+
email: email.trim().toLowerCase(),
|
|
346
|
+
name: name.trim()
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
[guestProfileKey]
|
|
353
|
+
);
|
|
354
|
+
const confirmGuestContact = useCallback(
|
|
355
|
+
(invalidEmailMessage) => {
|
|
356
|
+
reportError(null);
|
|
357
|
+
if (isAuthenticated) {
|
|
358
|
+
setGuestContactOk(true);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (!isValidEmail(guestEmail)) {
|
|
362
|
+
reportError(
|
|
363
|
+
invalidEmailMessage ?? "Please enter a valid email address."
|
|
364
|
+
);
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
persistGuestProfile(guestEmail, guestName);
|
|
368
|
+
setGuestContactOk(true);
|
|
369
|
+
return true;
|
|
370
|
+
},
|
|
371
|
+
[
|
|
372
|
+
isAuthenticated,
|
|
373
|
+
guestEmail,
|
|
374
|
+
guestName,
|
|
375
|
+
persistGuestProfile,
|
|
376
|
+
reportError
|
|
377
|
+
]
|
|
378
|
+
);
|
|
379
|
+
const resetGuestContact = useCallback(() => {
|
|
380
|
+
setGuestContactOk(false);
|
|
381
|
+
reportError(null);
|
|
382
|
+
}, [reportError]);
|
|
383
|
+
const mergeIncoming = useCallback((rows) => {
|
|
384
|
+
const parsed = parseChatMessages(rows);
|
|
385
|
+
setMessages((prev) => {
|
|
386
|
+
const next = mergeChatMessages(prev, parsed);
|
|
387
|
+
sinceRef.current = latestMessageCreatedAt(next);
|
|
388
|
+
return next;
|
|
389
|
+
});
|
|
390
|
+
}, []);
|
|
391
|
+
const mergeMessage = useCallback((message) => {
|
|
392
|
+
setMessages((prev) => {
|
|
393
|
+
const next = mergeChatMessages(prev, [message]);
|
|
394
|
+
sinceRef.current = latestMessageCreatedAt(next);
|
|
395
|
+
return next;
|
|
396
|
+
});
|
|
397
|
+
}, []);
|
|
398
|
+
const clearSession = useCallback(() => {
|
|
399
|
+
try {
|
|
400
|
+
sessionStorage.removeItem(storageKey);
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
setSessionPublicId(null);
|
|
404
|
+
sessionPublicIdRef.current = null;
|
|
405
|
+
setMessages([]);
|
|
406
|
+
sinceRef.current = void 0;
|
|
407
|
+
}, [storageKey]);
|
|
408
|
+
const stopRealtime = useCallback(() => {
|
|
409
|
+
if (pollRef.current) {
|
|
410
|
+
clearInterval(pollRef.current);
|
|
411
|
+
pollRef.current = null;
|
|
412
|
+
}
|
|
413
|
+
wsRef.current?.close();
|
|
414
|
+
wsRef.current = null;
|
|
415
|
+
setConnectionState("closed");
|
|
416
|
+
}, []);
|
|
417
|
+
const refreshMessages = useCallback(
|
|
418
|
+
async (sid, opts) => {
|
|
419
|
+
const id = sid ?? sessionPublicIdRef.current;
|
|
420
|
+
if (!id) return;
|
|
421
|
+
if (faultRef.current) return;
|
|
422
|
+
const params = new URLSearchParams({ sessionPublicId: id });
|
|
423
|
+
if (!opts?.full && sinceRef.current) {
|
|
424
|
+
params.set("since", sinceRef.current);
|
|
425
|
+
}
|
|
426
|
+
let res;
|
|
427
|
+
try {
|
|
428
|
+
res = await fetch(`${messagesEndpoint}?${params.toString()}`);
|
|
429
|
+
} catch {
|
|
430
|
+
const next = classifyChatFault(0, null, "Network error");
|
|
431
|
+
if (next) {
|
|
432
|
+
reportFault(next);
|
|
433
|
+
stopRealtime();
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const data = await res.json().catch(() => ({}));
|
|
438
|
+
if (!res.ok) {
|
|
439
|
+
if (isChatSessionNotFound(res.status, data)) {
|
|
440
|
+
clearSession();
|
|
441
|
+
reportError(null);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const classified = classifyChatFault(
|
|
445
|
+
res.status,
|
|
446
|
+
data,
|
|
447
|
+
"Failed to load messages"
|
|
448
|
+
);
|
|
449
|
+
if (classified) {
|
|
450
|
+
reportFault(classified);
|
|
451
|
+
stopRealtime();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
reportError(messageFromPayload(data, "Failed to load messages"));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (Array.isArray(data.data)) {
|
|
458
|
+
mergeIncoming(data.data);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
[
|
|
462
|
+
messagesEndpoint,
|
|
463
|
+
mergeIncoming,
|
|
464
|
+
clearSession,
|
|
465
|
+
reportError,
|
|
466
|
+
reportFault,
|
|
467
|
+
stopRealtime
|
|
468
|
+
]
|
|
469
|
+
);
|
|
470
|
+
useEffect(() => {
|
|
471
|
+
const realtimeOn = enabled && enableRealtime && guestContactOk && !fault;
|
|
472
|
+
if (!realtimeOn || !sessionPublicId) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const connectWs = () => {
|
|
476
|
+
if (!enableWebSocket || !apiOrigin || !projectUid || !integrationId) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const url = buildVisitorWsUrl(
|
|
481
|
+
apiOrigin,
|
|
482
|
+
projectUid,
|
|
483
|
+
integrationId,
|
|
484
|
+
sessionPublicId
|
|
485
|
+
);
|
|
486
|
+
setConnectionState("connecting");
|
|
487
|
+
const ws = new WebSocket(url);
|
|
488
|
+
wsRef.current = ws;
|
|
489
|
+
ws.onopen = () => setConnectionState("open");
|
|
490
|
+
ws.onmessage = (ev) => {
|
|
491
|
+
try {
|
|
492
|
+
const msg = JSON.parse(ev.data);
|
|
493
|
+
if (msg.type !== "chat.message") return;
|
|
494
|
+
const p = msg.payload;
|
|
495
|
+
if (p && typeof p.messageId === "string" && typeof p.content === "string") {
|
|
496
|
+
mergeMessage({
|
|
497
|
+
id: p.messageId,
|
|
498
|
+
content: p.content,
|
|
499
|
+
messageType: typeof p.messageType === "string" ? p.messageType : "OUTBOUND",
|
|
500
|
+
senderEmail: typeof p.senderEmail === "string" ? p.senderEmail : null,
|
|
501
|
+
senderName: typeof p.senderName === "string" ? p.senderName : null,
|
|
502
|
+
createdAt: typeof p.createdAt === "string" ? p.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
void refreshMessages(sessionPublicId);
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
ws.onclose = () => {
|
|
510
|
+
wsRef.current = null;
|
|
511
|
+
setConnectionState("closed");
|
|
512
|
+
};
|
|
513
|
+
} catch {
|
|
514
|
+
setConnectionState("closed");
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
connectWs();
|
|
518
|
+
void refreshMessages(sessionPublicId, { full: true });
|
|
519
|
+
if (enablePolling) {
|
|
520
|
+
pollRef.current = setInterval(() => {
|
|
521
|
+
void refreshMessages(sessionPublicId);
|
|
522
|
+
}, pollIntervalMs);
|
|
523
|
+
}
|
|
524
|
+
return () => {
|
|
525
|
+
if (pollRef.current) clearInterval(pollRef.current);
|
|
526
|
+
pollRef.current = null;
|
|
527
|
+
wsRef.current?.close();
|
|
528
|
+
wsRef.current = null;
|
|
529
|
+
setConnectionState("idle");
|
|
530
|
+
};
|
|
531
|
+
}, [
|
|
532
|
+
enabled,
|
|
533
|
+
enableRealtime,
|
|
534
|
+
enablePolling,
|
|
535
|
+
enableWebSocket,
|
|
536
|
+
guestContactOk,
|
|
537
|
+
fault,
|
|
538
|
+
sessionPublicId,
|
|
539
|
+
apiOrigin,
|
|
540
|
+
projectUid,
|
|
541
|
+
integrationId,
|
|
542
|
+
pollIntervalMs,
|
|
543
|
+
refreshMessages,
|
|
544
|
+
mergeMessage
|
|
545
|
+
]);
|
|
546
|
+
const sendMessage = useCallback(
|
|
547
|
+
async (content, opts) => {
|
|
548
|
+
const text = content.trim();
|
|
549
|
+
if (!text || sending) return false;
|
|
550
|
+
if (faultRef.current) return false;
|
|
551
|
+
if (!isAuthenticated && !isValidEmail(guestEmail)) {
|
|
552
|
+
reportError(
|
|
553
|
+
opts?.invalidEmailMessage ?? "Please enter a valid email address."
|
|
554
|
+
);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
setSending(true);
|
|
558
|
+
reportError(null);
|
|
559
|
+
clearFault();
|
|
560
|
+
try {
|
|
561
|
+
const buildBody = (includeSession) => {
|
|
562
|
+
const body = { content: text };
|
|
563
|
+
const pageUrl = opts?.pageUrl ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
564
|
+
if (pageUrl) body.pageUrl = pageUrl;
|
|
565
|
+
const sid = includeSession ? sessionPublicIdRef.current : null;
|
|
566
|
+
if (sid) body.sessionPublicId = sid;
|
|
567
|
+
else if (opts?.topicId?.trim()) {
|
|
568
|
+
body.topicId = opts.topicId.trim();
|
|
569
|
+
}
|
|
570
|
+
if (!isAuthenticated) {
|
|
571
|
+
body.senderEmail = guestEmail.trim().toLowerCase();
|
|
572
|
+
if (guestName.trim()) body.senderName = guestName.trim();
|
|
573
|
+
}
|
|
574
|
+
return body;
|
|
575
|
+
};
|
|
576
|
+
const post = async (includeSession) => {
|
|
577
|
+
let res2;
|
|
578
|
+
try {
|
|
579
|
+
res2 = await fetch(submitEndpoint, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: { "Content-Type": "application/json" },
|
|
582
|
+
body: JSON.stringify(buildBody(includeSession))
|
|
583
|
+
});
|
|
584
|
+
} catch {
|
|
585
|
+
return {
|
|
586
|
+
res: null,
|
|
587
|
+
data: {},
|
|
588
|
+
networkError: true
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
const data2 = await res2.json().catch(() => ({}));
|
|
592
|
+
return { res: res2, data: data2, networkError: false };
|
|
593
|
+
};
|
|
594
|
+
let result = await post(true);
|
|
595
|
+
if (result.networkError) {
|
|
596
|
+
const next = classifyChatFault(0, null, "Network error");
|
|
597
|
+
if (next) {
|
|
598
|
+
reportFault(next);
|
|
599
|
+
stopRealtime();
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
let { res, data } = result;
|
|
604
|
+
if (!res) return false;
|
|
605
|
+
if (!res.ok && isChatSessionNotFound(res.status, data)) {
|
|
606
|
+
clearSession();
|
|
607
|
+
result = await post(false);
|
|
608
|
+
if (result.networkError) {
|
|
609
|
+
const next = classifyChatFault(0, null, "Network error");
|
|
610
|
+
if (next) {
|
|
611
|
+
reportFault(next);
|
|
612
|
+
stopRealtime();
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
;
|
|
617
|
+
({ res, data } = result);
|
|
618
|
+
if (!res) return false;
|
|
619
|
+
}
|
|
620
|
+
if (!res.ok) {
|
|
621
|
+
const classified = classifyChatFault(
|
|
622
|
+
res.status,
|
|
623
|
+
data,
|
|
624
|
+
"Send failed"
|
|
625
|
+
);
|
|
626
|
+
if (classified) {
|
|
627
|
+
reportFault(classified);
|
|
628
|
+
stopRealtime();
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
reportError(messageFromPayload(data, "Send failed"));
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if (data.sessionPublicId) {
|
|
635
|
+
persistSessionPublicId(data.sessionPublicId, storageKey, {
|
|
636
|
+
sessionPublicIdRef,
|
|
637
|
+
setSessionPublicId,
|
|
638
|
+
onCreated: onSessionCreatedRef.current
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (!isAuthenticated) {
|
|
642
|
+
persistGuestProfile(guestEmail, guestName);
|
|
643
|
+
}
|
|
644
|
+
if (typeof data.messageId === "string" && data.messageId) {
|
|
645
|
+
mergeMessage({
|
|
646
|
+
id: data.messageId,
|
|
647
|
+
content: text,
|
|
648
|
+
messageType: "INBOUND",
|
|
649
|
+
senderEmail: isAuthenticated ? null : guestEmail.trim().toLowerCase(),
|
|
650
|
+
senderName: isAuthenticated ? null : guestName.trim() || null,
|
|
651
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const nextSid = data.sessionPublicId ?? sessionPublicIdRef.current;
|
|
655
|
+
if (nextSid) {
|
|
656
|
+
await refreshMessages(nextSid, { full: true });
|
|
657
|
+
}
|
|
658
|
+
onMessageSentRef.current?.(text);
|
|
659
|
+
return true;
|
|
660
|
+
} catch (e) {
|
|
661
|
+
reportError(e instanceof Error ? e.message : "Send failed");
|
|
662
|
+
return false;
|
|
663
|
+
} finally {
|
|
664
|
+
setSending(false);
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
[
|
|
668
|
+
sending,
|
|
669
|
+
isAuthenticated,
|
|
670
|
+
guestEmail,
|
|
671
|
+
guestName,
|
|
672
|
+
submitEndpoint,
|
|
673
|
+
storageKey,
|
|
674
|
+
persistGuestProfile,
|
|
675
|
+
mergeMessage,
|
|
676
|
+
refreshMessages,
|
|
677
|
+
clearSession,
|
|
678
|
+
reportError,
|
|
679
|
+
reportFault,
|
|
680
|
+
clearFault,
|
|
681
|
+
stopRealtime
|
|
682
|
+
]
|
|
683
|
+
);
|
|
684
|
+
const retryAfterFault = useCallback(() => {
|
|
685
|
+
clearFault();
|
|
686
|
+
reportError(null);
|
|
687
|
+
}, [clearFault, reportError]);
|
|
688
|
+
const canCompose = isAuthenticated || guestProfileHydrated && guestContactOk;
|
|
689
|
+
return {
|
|
690
|
+
sessionPublicId,
|
|
691
|
+
messages,
|
|
692
|
+
sending,
|
|
693
|
+
error,
|
|
694
|
+
fault,
|
|
695
|
+
setError: reportError,
|
|
696
|
+
clearFault,
|
|
697
|
+
retryAfterFault,
|
|
698
|
+
connectionState,
|
|
699
|
+
guestEmail,
|
|
700
|
+
guestName,
|
|
701
|
+
setGuestEmail,
|
|
702
|
+
setGuestName,
|
|
703
|
+
guestContactOk,
|
|
704
|
+
guestProfileHydrated,
|
|
705
|
+
confirmGuestContact,
|
|
706
|
+
resetGuestContact,
|
|
707
|
+
canCompose,
|
|
708
|
+
sendMessage,
|
|
709
|
+
refreshMessages,
|
|
710
|
+
clearSession
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/hooks/index.ts
|
|
715
|
+
function useMailooChatSubmit(options) {
|
|
716
|
+
const endpoint = options?.endpoint ?? DEFAULT_CHAT_SUBMIT_ENDPOINT;
|
|
717
|
+
const [pending, setPending] = useState2(false);
|
|
718
|
+
const submit = useCallback2(
|
|
719
|
+
async (body) => {
|
|
720
|
+
setPending(true);
|
|
721
|
+
try {
|
|
722
|
+
const res = await fetch(endpoint, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers: { "Content-Type": "application/json" },
|
|
725
|
+
body: JSON.stringify(body)
|
|
726
|
+
});
|
|
727
|
+
const data = await res.json().catch(() => ({}));
|
|
728
|
+
if (!res.ok) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
status: res.status,
|
|
732
|
+
message: messageFromPayload(data, "Send failed")
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return { ok: true, status: res.status, data };
|
|
736
|
+
} catch {
|
|
737
|
+
return { ok: false, status: 0, message: "Network error" };
|
|
738
|
+
} finally {
|
|
739
|
+
setPending(false);
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
[endpoint]
|
|
743
|
+
);
|
|
744
|
+
return { submit, pending };
|
|
745
|
+
}
|
|
746
|
+
function useMailooChatMessages(options) {
|
|
747
|
+
const endpoint = options?.endpoint ?? DEFAULT_CHAT_MESSAGES_ENDPOINT;
|
|
748
|
+
const [pending, setPending] = useState2(false);
|
|
749
|
+
const refresh = useCallback2(
|
|
750
|
+
async (sessionPublicId, since) => {
|
|
751
|
+
setPending(true);
|
|
752
|
+
try {
|
|
753
|
+
const params = new URLSearchParams({ sessionPublicId });
|
|
754
|
+
if (since) params.set("since", since);
|
|
755
|
+
const res = await fetch(`${endpoint}?${params.toString()}`);
|
|
756
|
+
const data = await res.json().catch(() => ({}));
|
|
757
|
+
if (!res.ok) {
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
status: res.status,
|
|
761
|
+
message: messageFromPayload(data, "Fetch failed")
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const rows = Array.isArray(data.data) ? data.data : [];
|
|
765
|
+
return {
|
|
766
|
+
ok: true,
|
|
767
|
+
status: res.status,
|
|
768
|
+
data,
|
|
769
|
+
messages: parseChatMessages(rows)
|
|
770
|
+
};
|
|
771
|
+
} catch {
|
|
772
|
+
return { ok: false, status: 0, message: "Network error" };
|
|
773
|
+
} finally {
|
|
774
|
+
setPending(false);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
[endpoint]
|
|
778
|
+
);
|
|
779
|
+
return { refresh, pending };
|
|
780
|
+
}
|
|
781
|
+
function useMailooChatStatus(options) {
|
|
782
|
+
const endpoint = options?.endpoint ?? DEFAULT_CHAT_STATUS_ENDPOINT;
|
|
783
|
+
const enabled = options?.enabled ?? true;
|
|
784
|
+
const [status, setStatus] = useState2(null);
|
|
785
|
+
const [pending, setPending] = useState2(false);
|
|
786
|
+
const [error, setError] = useState2(null);
|
|
787
|
+
const refresh = useCallback2(async () => {
|
|
788
|
+
setPending(true);
|
|
789
|
+
setError(null);
|
|
790
|
+
try {
|
|
791
|
+
const res = await fetch(endpoint);
|
|
792
|
+
const data = await res.json().catch(() => ({}));
|
|
793
|
+
if (!res.ok) {
|
|
794
|
+
setError(messageFromPayload(data, "Status fetch failed"));
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
if (typeof data.status !== "string" || typeof data.name !== "string" || typeof data.type !== "string" || typeof data.lastActivity !== "string") {
|
|
798
|
+
setError("Invalid status payload");
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
const parsed = {
|
|
802
|
+
status: data.status,
|
|
803
|
+
name: data.name,
|
|
804
|
+
type: data.type,
|
|
805
|
+
lastActivity: data.lastActivity
|
|
806
|
+
};
|
|
807
|
+
setStatus(parsed);
|
|
808
|
+
return parsed;
|
|
809
|
+
} catch {
|
|
810
|
+
setError("Network error");
|
|
811
|
+
return null;
|
|
812
|
+
} finally {
|
|
813
|
+
setPending(false);
|
|
814
|
+
}
|
|
815
|
+
}, [endpoint]);
|
|
816
|
+
useEffect2(() => {
|
|
817
|
+
if (!enabled) return;
|
|
818
|
+
void refresh();
|
|
819
|
+
}, [enabled, refresh]);
|
|
820
|
+
return { status, pending, error, refresh };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export {
|
|
824
|
+
DEFAULT_CHAT_SUBMIT_ENDPOINT,
|
|
825
|
+
DEFAULT_CHAT_MESSAGES_ENDPOINT,
|
|
826
|
+
DEFAULT_CHAT_STATUS_ENDPOINT,
|
|
827
|
+
DEFAULT_CHAT_SESSION_STORAGE_KEY,
|
|
828
|
+
DEFAULT_CHAT_GUEST_PROFILE_KEY,
|
|
829
|
+
DEFAULT_CHAT_POLL_INTERVAL_MS,
|
|
830
|
+
messageFromPayload,
|
|
831
|
+
classifyChatFault,
|
|
832
|
+
isChatSessionNotFound,
|
|
833
|
+
defaultChatUiLabels,
|
|
834
|
+
useMailooChatSession,
|
|
835
|
+
useMailooChatSubmit,
|
|
836
|
+
useMailooChatMessages,
|
|
837
|
+
useMailooChatStatus
|
|
838
|
+
};
|
|
839
|
+
//# sourceMappingURL=chunk-QZ7HWQDO.js.map
|