@fluid-app/portal-sdk 0.1.99 → 0.1.100

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.
@@ -69,742 +69,6 @@ function useMessagingAuth() {
69
69
  };
70
70
  }
71
71
  //#endregion
72
- //#region src/messaging/use-messaging-config.ts
73
- /**
74
- * Hook that derives MessagingApiConfig from the portal SDK's FluidProvider context.
75
- *
76
- * Maps FluidSDKConfig fields to the shape expected by MessagingApp:
77
- * - baseUrl -> from config.baseUrl
78
- * - getHeaders -> builds Authorization header from config.getAuthToken()
79
- * - onAuthError -> from config.onAuthError
80
- * - websocketUrl -> config.websocketUrl or derived from baseUrl
81
- * - token -> from auth context
82
- */
83
- function deriveWebsocketUrl(baseUrl) {
84
- return `${baseUrl.replace(/\/+$/, "").replace(/\/api$/, "")}/cable`;
85
- }
86
- function useMessagingConfig() {
87
- const { config } = require_FluidProvider.useFluidContext();
88
- const auth = require_FluidProvider.useFluidAuthContext();
89
- const getHeaders = (0, react.useCallback)(async () => {
90
- const headers = {
91
- "Content-Type": "application/json",
92
- ...config.defaultHeaders
93
- };
94
- if (config.getAuthToken) {
95
- const token = await config.getAuthToken();
96
- if (token) headers.Authorization = `Bearer ${token}`;
97
- }
98
- return headers;
99
- }, [config]);
100
- const apiBaseUrl = (0, react.useMemo)(() => {
101
- const base = config.baseUrl.replace(/\/+$/, "");
102
- return base.endsWith("/api") ? base : `${base}/api`;
103
- }, [config.baseUrl]);
104
- return {
105
- apiConfig: (0, react.useMemo)(() => ({
106
- baseUrl: apiBaseUrl,
107
- getHeaders,
108
- ...config.onAuthError != null && { onAuthError: config.onAuthError }
109
- }), [
110
- apiBaseUrl,
111
- config.onAuthError,
112
- getHeaders
113
- ]),
114
- websocketUrl: (0, react.useMemo)(() => config.websocketUrl ?? deriveWebsocketUrl(config.baseUrl), [config.websocketUrl, config.baseUrl]),
115
- token: auth.token
116
- };
117
- }
118
- //#endregion
119
- //#region ../../messaging/core/src/query-keys.ts
120
- /**
121
- * Query key factory for all messaging-related TanStack Query queries.
122
- * Ensures consistent key shapes across hooks and cache helpers.
123
- */
124
- const MESSAGES_QUERY_KEYS = {
125
- all: () => ["messages"],
126
- conversationMetadata: (conversationId) => [
127
- ...MESSAGES_QUERY_KEYS.all(),
128
- "conversation",
129
- conversationId
130
- ],
131
- newMessageFlag: (conversationId) => [
132
- ...MESSAGES_QUERY_KEYS.all(),
133
- "conversation",
134
- conversationId,
135
- "new_message_flag"
136
- ],
137
- listConversations: () => [...MESSAGES_QUERY_KEYS.all(), "conversations"],
138
- listMessages: (conversationId, messageId, startAtISO) => [
139
- ...MESSAGES_QUERY_KEYS.all(),
140
- "conversation",
141
- conversationId,
142
- "messages",
143
- messageId ? { startFromMessage: messageId } : startAtISO ? { startFromDate: startAtISO } : { startFromBottom: true }
144
- ],
145
- listPinnedMessages: (conversationId) => [
146
- ...MESSAGES_QUERY_KEYS.all(),
147
- "conversation",
148
- conversationId,
149
- "pinned"
150
- ],
151
- outbox: (conversationId) => [
152
- ...MESSAGES_QUERY_KEYS.all(),
153
- "conversation",
154
- conversationId,
155
- "outbox"
156
- ],
157
- listScheduledMessages: (conversationId) => [
158
- ...MESSAGES_QUERY_KEYS.all(),
159
- "conversation",
160
- conversationId,
161
- "scheduled"
162
- ],
163
- listMyScheduledMessages: () => [
164
- ...MESSAGES_QUERY_KEYS.all(),
165
- "scheduled",
166
- "mine"
167
- ],
168
- draft: (conversationId) => [
169
- ...MESSAGES_QUERY_KEYS.all(),
170
- "draft",
171
- conversationId
172
- ],
173
- announcementChannel: () => [...MESSAGES_QUERY_KEYS.all(), "announcement-channel"],
174
- downline: () => [...MESSAGES_QUERY_KEYS.all(), "downline"]
175
- };
176
- //#endregion
177
- //#region ../../messaging/core/src/constants.ts
178
- const WEBSOCKET_QUERY_OPTIONS = {
179
- staleTime: Infinity,
180
- refetchOnMount: "always",
181
- refetchOnReconnect: "always",
182
- refetchOnWindowFocus: false
183
- };
184
- /**
185
- * Scheduled messages are not websocket-driven; poll to keep fresh.
186
- */
187
- const SCHEDULED_MESSAGES_STALE_TIME = 6e4;
188
- const SCHEDULED_MESSAGES_REFETCH_INTERVAL = 6e4;
189
- /**
190
- * Pinned messages are not websocket-driven; poll to keep fresh.
191
- */
192
- const PINNED_MESSAGES_STALE_TIME = 6e4;
193
- const PINNED_MESSAGES_REFETCH_INTERVAL = 6e4;
194
- //#endregion
195
- //#region ../../messaging/core/src/cache-helpers.ts
196
- /**
197
- * seed conversation metadata caches from any conversations list queries.
198
- * returns the matched conversation if conversationId is provided and present in any list cache.
199
- */
200
- function seedConversationMetadataFromLists(queryClient, conversationId) {
201
- let matched;
202
- const seedFromData = (data) => {
203
- const pages = data?.pages ?? [];
204
- for (const page of pages) {
205
- const items = page?.[1]?.items ?? [];
206
- for (const item of items) {
207
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.conversationMetadata(item.id), item);
208
- if (!matched && typeof conversationId === "number" && item.id === conversationId) matched = item;
209
- }
210
- }
211
- };
212
- seedFromData(queryClient.getQueryData(MESSAGES_QUERY_KEYS.listConversations()));
213
- const listQueries = queryClient.getQueryCache().findAll({ predicate: (q) => {
214
- const key = q.queryKey;
215
- return Array.isArray(key) && key.length >= 2 && key[0] === MESSAGES_QUERY_KEYS.all()[0] && key[1] === MESSAGES_QUERY_KEYS.listConversations()[1];
216
- } });
217
- for (const q of listQueries) seedFromData(queryClient.getQueryData(q.queryKey));
218
- return matched;
219
- }
220
- function isMessagesQueryKeyForConversation(queryKey, conversationId) {
221
- if (!Array.isArray(queryKey)) return false;
222
- return queryKey.length >= 4 && queryKey[0] === MESSAGES_QUERY_KEYS.all()[0] && queryKey[1] === "conversation" && queryKey[2] === conversationId && queryKey[3] === "messages";
223
- }
224
- function findMessageInCache(queryClient, conversationId, messageId) {
225
- const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) });
226
- for (const query of queries) {
227
- const data = queryClient.getQueryData(query.queryKey);
228
- if (data) for (const page of data.pages) {
229
- const message = page[1].items.find((m) => m.id === messageId);
230
- if (message) return message;
231
- }
232
- }
233
- }
234
- function forEachMessagesQuery(queryClient, conversationId, fn) {
235
- queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) }).forEach((q) => fn(q.queryKey));
236
- }
237
- function isBottomAnchorKey(queryKey) {
238
- if (!Array.isArray(queryKey)) return false;
239
- const anchor = queryKey[queryKey.length - 1];
240
- if (typeof anchor === "object" && anchor !== null) {
241
- if ("startFromBottom" in anchor) return anchor.startFromBottom === true;
242
- }
243
- return false;
244
- }
245
- function isPinnedQueryKeyForConversation(queryKey, conversationId) {
246
- if (!Array.isArray(queryKey)) return false;
247
- return queryKey.length >= 4 && queryKey[0] === MESSAGES_QUERY_KEYS.all()[0] && queryKey[1] === "conversation" && queryKey[2] === conversationId && queryKey[3] === "pinned";
248
- }
249
- function forEachPinnedQuery(queryClient, conversationId, fn) {
250
- queryClient.getQueryCache().findAll({ predicate: (q) => isPinnedQueryKeyForConversation(q.queryKey, conversationId) }).forEach((q) => fn(q.queryKey));
251
- }
252
- async function cancelMessageQueries(queryClient, conversationId) {
253
- const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) });
254
- await Promise.all(queries.map((q) => queryClient.cancelQueries({ queryKey: q.queryKey })));
255
- }
256
- async function cancelPinnedQueries(queryClient, conversationId) {
257
- const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isPinnedQueryKeyForConversation(q.queryKey, conversationId) });
258
- await Promise.all(queries.map((q) => queryClient.cancelQueries({ queryKey: q.queryKey })));
259
- }
260
- function applyMessagesOptimisticSend({ queryClient, conversationId, optimisticMessage }) {
261
- forEachMessagesQuery(queryClient, conversationId, (key) => {
262
- if (!isBottomAnchorKey(key)) return;
263
- queryClient.setQueryData(key, (oldData) => {
264
- const newOptimisticPage = [{ pagination: {
265
- current: 1,
266
- previous: null,
267
- next: null,
268
- per_page: oldData?.pages?.[0]?.[0]?.pagination?.per_page ?? 20,
269
- pages: oldData?.pages?.[0]?.[0]?.pagination?.pages ?? 1,
270
- count: oldData?.pages?.[0]?.[0]?.pagination?.count ?? 0
271
- } }, { items: [optimisticMessage] }];
272
- if (!oldData) return {
273
- pages: [newOptimisticPage],
274
- pageParams: [1]
275
- };
276
- const pages = oldData.pages.map((page, idx) => {
277
- if (idx === 0) {
278
- const currentItems = page?.[1]?.items ?? [];
279
- return [{ pagination: {
280
- ...page?.[0]?.pagination ?? {},
281
- count: page?.[0]?.pagination?.count ?? 0
282
- } }, { items: [optimisticMessage, ...currentItems] }];
283
- }
284
- return page;
285
- });
286
- return {
287
- ...oldData,
288
- pages
289
- };
290
- });
291
- });
292
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
293
- const prev = old?.items ?? [];
294
- if (prev.some((m) => m.id === optimisticMessage.id)) return old;
295
- return { items: [...prev, optimisticMessage] };
296
- });
297
- }
298
- /**
299
- * markNewMessageFlagForConversation
300
- *
301
- * sets a lightweight flag query for a conversation indicating a new message
302
- * arrived while the user may not be at the bottom. UI can reset it after
303
- * scrolling or showing a pill.
304
- */
305
- function markNewMessageFlagForConversation(queryClient, conversationId) {
306
- queryClient.setQueryData([
307
- MESSAGES_QUERY_KEYS.all()[0],
308
- "conversation",
309
- conversationId,
310
- "new_message_flag"
311
- ], true);
312
- }
313
- function clearNewMessageFlagForConversation(queryClient, conversationId) {
314
- queryClient.setQueryData([
315
- MESSAGES_QUERY_KEYS.all()[0],
316
- "conversation",
317
- conversationId,
318
- "new_message_flag"
319
- ], false);
320
- }
321
- function applyMessageUpsertToCache({ queryClient, conversationId, message, replaceOptimisticId, clientMessageId }) {
322
- forEachMessagesQuery(queryClient, conversationId, (key) => {
323
- queryClient.setQueryData(key, (oldData) => {
324
- if (!oldData?.pages?.length) {
325
- if (!isBottomAnchorKey(key)) return oldData;
326
- return {
327
- pages: [[{ pagination: {
328
- current: 1,
329
- previous: null,
330
- next: null,
331
- per_page: 20,
332
- pages: 1,
333
- count: 1
334
- } }, { items: [message] }]],
335
- pageParams: [1]
336
- };
337
- }
338
- const normalizedClientMessageId = (() => {
339
- if (typeof clientMessageId === "string" && clientMessageId.length > 0) return clientMessageId;
340
- const fromMeta = (() => {
341
- const meta = message.metadata;
342
- if (meta && typeof meta === "object" && "client_message_id" in meta) {
343
- const id = meta.client_message_id;
344
- if (typeof id === "string" || typeof id === "number" && Number.isFinite(id)) return id;
345
- }
346
- })();
347
- if (typeof fromMeta === "string" || typeof fromMeta === "number" && Number.isFinite(fromMeta)) return String(fromMeta);
348
- })();
349
- const finalMessage = (() => {
350
- if (!normalizedClientMessageId) return message;
351
- const currentMeta = message.metadata;
352
- if (currentMeta != null && typeof currentMeta === "object" && (typeof currentMeta.client_message_id === "string" || typeof currentMeta.client_message_id === "number")) return message;
353
- return {
354
- ...message,
355
- metadata: {
356
- ...currentMeta && typeof currentMeta === "object" ? currentMeta : {},
357
- client_message_id: normalizedClientMessageId
358
- }
359
- };
360
- })();
361
- let replaced = false;
362
- const pages = oldData.pages.map((page) => {
363
- const [meta, payload] = page;
364
- const items = payload.items.slice();
365
- const idxById = items.findIndex((m) => m.id === (replaceOptimisticId ?? message.id));
366
- if (idxById !== -1) {
367
- items[idxById] = finalMessage;
368
- replaced = true;
369
- return [meta, { items }];
370
- }
371
- if (normalizedClientMessageId) {
372
- const idxByClientId = items.findIndex((m) => typeof m?.metadata === "object" && m?.metadata != null && ((id) => id != null && String(id) === normalizedClientMessageId)(m.metadata?.client_message_id));
373
- if (idxByClientId !== -1) {
374
- items[idxByClientId] = finalMessage;
375
- replaced = true;
376
- return [meta, { items }];
377
- }
378
- }
379
- const idxExisting = items.findIndex((m) => m.id === message.id);
380
- if (idxExisting !== -1) {
381
- items[idxExisting] = finalMessage;
382
- replaced = true;
383
- return [meta, { items }];
384
- }
385
- return [meta, { items }];
386
- });
387
- if (!replaced) {
388
- if (!isBottomAnchorKey(key)) return oldData;
389
- const first = pages[0];
390
- const nextPages = pages.slice();
391
- const newFirstItems = [finalMessage, ...first?.[1]?.items ?? []];
392
- const newCount = (first?.[0]?.pagination?.count ?? 0) + 1;
393
- nextPages[0] = [{ pagination: {
394
- ...first?.[0]?.pagination ?? {},
395
- count: newCount
396
- } }, { items: newFirstItems }];
397
- return {
398
- ...oldData,
399
- pages: nextPages
400
- };
401
- }
402
- return {
403
- ...oldData,
404
- pages
405
- };
406
- });
407
- });
408
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
409
- if (!old?.items) return old;
410
- return { items: old.items.filter((m) => m.id !== (replaceOptimisticId ?? -99999999)) };
411
- });
412
- }
413
- function applyMessageDeleteFromCache({ queryClient, conversationId, messageId }) {
414
- forEachMessagesQuery(queryClient, conversationId, (key) => {
415
- queryClient.setQueryData(key, (oldData) => {
416
- if (!oldData) return oldData;
417
- const pages = oldData.pages.map((page) => {
418
- const [meta, payload] = page;
419
- const items = payload.items.filter((m) => m.id !== messageId);
420
- const newCount = Math.max(0, (meta?.pagination?.count ?? 0) - (payload.items.length - items.length));
421
- return [{ pagination: {
422
- ...meta?.pagination ?? {},
423
- count: newCount
424
- } }, { items }];
425
- });
426
- return {
427
- ...oldData,
428
- pages
429
- };
430
- });
431
- });
432
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
433
- if (!old?.items) return old;
434
- return { items: old.items.filter((m) => m.id !== messageId) };
435
- });
436
- }
437
- function applyReactionUpdateToCache({ queryClient, conversationId, messageId, reaction, reaction_stats }) {
438
- forEachMessagesQuery(queryClient, conversationId, (key) => {
439
- queryClient.setQueryData(key, (oldData) => {
440
- if (!oldData) return oldData;
441
- const pages = oldData.pages.map((page) => {
442
- const [meta, payload] = page;
443
- return [meta, { items: payload.items.map((m) => m.id === messageId ? {
444
- ...m,
445
- reaction,
446
- reaction_stats
447
- } : m) }];
448
- });
449
- return {
450
- ...oldData,
451
- pages
452
- };
453
- });
454
- });
455
- forEachPinnedQuery(queryClient, conversationId, (key) => {
456
- queryClient.setQueryData(key, (oldData) => {
457
- if (!oldData) return oldData;
458
- const pages = oldData.pages.map((page) => {
459
- const [meta, payload] = page;
460
- return [meta, { items: payload.items.map((m) => m.id === messageId ? {
461
- ...m,
462
- reaction,
463
- reaction_stats
464
- } : m) }];
465
- });
466
- return {
467
- ...oldData,
468
- pages
469
- };
470
- });
471
- });
472
- }
473
- function applyReactionStatsToCache({ queryClient, conversationId, messageId, reaction_stats }) {
474
- forEachMessagesQuery(queryClient, conversationId, (key) => {
475
- queryClient.setQueryData(key, (oldData) => {
476
- if (!oldData) return;
477
- const pages = oldData.pages.map((page) => {
478
- const [meta, payload] = page;
479
- return [meta, { items: payload.items.map((m) => m.id === messageId ? {
480
- ...m,
481
- reaction_stats
482
- } : m) }];
483
- });
484
- return {
485
- ...oldData,
486
- pages
487
- };
488
- });
489
- });
490
- forEachPinnedQuery(queryClient, conversationId, (key) => {
491
- queryClient.setQueryData(key, (oldData) => {
492
- if (!oldData) return oldData;
493
- const pages = oldData.pages.map((page) => {
494
- const [meta, payload] = page;
495
- return [meta, { items: payload.items.map((m) => m.id === messageId ? {
496
- ...m,
497
- reaction_stats
498
- } : m) }];
499
- });
500
- return {
501
- ...oldData,
502
- pages
503
- };
504
- });
505
- });
506
- }
507
- function applyConversationMetaToCaches({ queryClient, conversation }) {
508
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.conversationMetadata(conversation.id), (old) => {
509
- if (!old) return old;
510
- return {
511
- ...old,
512
- ...conversation
513
- };
514
- });
515
- const listQueries = queryClient.getQueryCache().findAll({ predicate: (q) => {
516
- const key = q.queryKey;
517
- return Array.isArray(key) && key.length >= 2 && key[0] === MESSAGES_QUERY_KEYS.all()[0] && key[1] === MESSAGES_QUERY_KEYS.listConversations()[1];
518
- } });
519
- for (const q of listQueries) queryClient.setQueryData(q.queryKey, (oldData) => {
520
- const data = oldData;
521
- if (!data?.pages) return oldData;
522
- return {
523
- ...data,
524
- pages: data.pages.map((page) => [page[0], {
525
- ...page[1],
526
- items: page[1].items.map((conv) => conv.id === conversation.id ? {
527
- ...conv,
528
- ...typeof conversation.unread === "boolean" ? { unread: conversation.unread } : {},
529
- ...typeof conversation.unread_messages_count === "number" ? { unread_messages_count: conversation.unread_messages_count } : {}
530
- } : conv)
531
- }])
532
- };
533
- });
534
- }
535
- function applyConversationDestroyed({ queryClient, conversationId }) {
536
- queryClient.removeQueries({ queryKey: MESSAGES_QUERY_KEYS.conversationMetadata(conversationId) });
537
- queryClient.setQueryData(MESSAGES_QUERY_KEYS.listConversations(), (oldData) => {
538
- const data = oldData;
539
- if (!data?.pages) return oldData;
540
- return {
541
- ...data,
542
- pages: data.pages.map((page) => [page[0], {
543
- ...page[1],
544
- items: page[1].items.filter((c) => c.id !== conversationId)
545
- }])
546
- };
547
- });
548
- }
549
- function applyMessagePinnedFlag({ queryClient, conversationId, messageId, pinned, message }) {
550
- forEachMessagesQuery(queryClient, conversationId, (key) => {
551
- queryClient.setQueryData(key, (oldData) => {
552
- if (!oldData) return oldData;
553
- const pages = oldData.pages.map((page) => {
554
- const [meta, payload] = page;
555
- return [meta, { items: payload.items.map((m) => m.id === messageId ? {
556
- ...m,
557
- pinned
558
- } : m) }];
559
- });
560
- return {
561
- ...oldData,
562
- pages
563
- };
564
- });
565
- });
566
- forEachPinnedQuery(queryClient, conversationId, (key) => {
567
- queryClient.setQueryData(key, (oldData) => {
568
- if (!oldData) return oldData;
569
- const pages = oldData.pages.map((page, idx) => {
570
- const [meta, payload] = page;
571
- if (idx !== 0) return page;
572
- const exists = payload.items.some((m) => m.id === messageId);
573
- if (pinned) {
574
- if (exists) return page;
575
- const msg = message ?? payload.items.find((m) => m.id === messageId);
576
- if (!msg) return page;
577
- return [meta, { items: [msg, ...payload.items] }];
578
- }
579
- return [meta, { items: payload.items.filter((m) => m.id !== messageId) }];
580
- });
581
- return {
582
- ...oldData,
583
- pages
584
- };
585
- });
586
- });
587
- }
588
- //#endregion
589
- //#region ../../messaging/core/src/utils/reaction-utils.ts
590
- /**
591
- * Splits an emoji string into an array of grapheme clusters (perceived characters/emojis).
592
- * Uses `Intl.Segmenter` when available for correct multi-codepoint emoji handling.
593
- * Falls back to `[...string]` (code-point iteration) on browsers without `Intl.Segmenter`
594
- * (e.g. Firefox < 119), which may incorrectly split ZWJ sequences, skin-tone modifiers,
595
- * and flag emojis into multiple entries.
596
- * @param emojiString The string of emojis to split.
597
- * @returns An array of strings, where each string is a single emoji.
598
- */
599
- function splitEmojis(emojiString) {
600
- if (!emojiString) return [];
601
- if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
602
- const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
603
- return Array.from(segmenter.segment(emojiString)).map((segment) => segment.segment);
604
- }
605
- return [...emojiString];
606
- }
607
- /**
608
- * Flattens reaction_stats to count individual emojis.
609
- * E.g., {"😀😂": 1, "👍": 2, "😀": 1} becomes {"😀": 2, "😂": 1, "👍": 2}
610
- * Handles multi-codepoint emojis correctly.
611
- * @param stats The raw reaction_stats object from the message.
612
- * @returns A record where keys are single emojis and values are their total counts.
613
- */
614
- function flattenReactionStats(stats) {
615
- if (!stats) return {};
616
- const flattened = {};
617
- for (const emojiKey in stats) {
618
- const count = stats[emojiKey];
619
- if (typeof count !== "number" || count === 0) continue;
620
- const individualEmojis = splitEmojis(emojiKey);
621
- for (const emoji of individualEmojis) flattened[emoji] = (flattened[emoji] || 0) + count;
622
- }
623
- return flattened;
624
- }
625
- /**
626
- * Calculates the optimistic state for a message's reactions when a user's reaction changes.
627
- * @param currentState The current reaction parts of the message ({ reaction, reaction_stats }).
628
- * @param newUserReaction The user's new complete reaction string (e.g., "😀👍", "😂", or null to unreact).
629
- * @returns An object with the updated `reaction` and `reaction_stats`.
630
- */
631
- function calculateOptimisticReactionUpdate(currentState, newUserReaction) {
632
- const previousUserReaction = currentState.reaction;
633
- const newStats = { ...currentState.reaction_stats || {} };
634
- const finalNewUserReaction = newUserReaction === "" ? null : newUserReaction;
635
- if (previousUserReaction === finalNewUserReaction) return {
636
- reaction: previousUserReaction,
637
- reaction_stats: newStats
638
- };
639
- if (previousUserReaction && typeof newStats[previousUserReaction] === "number") newStats[previousUserReaction] = Math.max(0, newStats[previousUserReaction] - 1);
640
- if (finalNewUserReaction) newStats[finalNewUserReaction] = (typeof newStats[finalNewUserReaction] === "number" ? newStats[finalNewUserReaction] : 0) + 1;
641
- return {
642
- reaction: finalNewUserReaction,
643
- reaction_stats: newStats
644
- };
645
- }
646
- /**
647
- * Generates a unique, sorted string from an array of emoji characters.
648
- * Each string in the input array is assumed to be a single, correctly segmented emoji.
649
- * E.g., ["😂", "😀", "😂"] becomes "😀😂"
650
- * @param emojis An array of single emoji strings (grapheme clusters).
651
- * @returns A sorted string of unique emojis, or an empty string if no emojis are provided.
652
- */
653
- function generateSortedEmojiStringFromArray(emojis) {
654
- if (!emojis || emojis.length === 0) return "";
655
- return [...new Set(emojis)].sort((a, b) => a.localeCompare(b, "en")).join("");
656
- }
657
- //#endregion
658
- //#region ../../messaging/core/src/utils/messaging.ts
659
- function mapConversationTypeToMessageType(conversationType) {
660
- switch (conversationType) {
661
- case "DirectConversation": return "DirectMessage";
662
- case "SmsConversation": return "SmsMessage";
663
- case "EmailConversation": return "EmailMessage";
664
- case "ChannelConversation":
665
- case "GroupConversation": return "DirectMessage";
666
- case "BotConversation": return "BotMessage";
667
- default: return "DirectMessage";
668
- }
669
- }
670
- /**
671
- * Determines the category of a chat. This is slightly different from the backend's
672
- * category field.
673
- *
674
- * @param item - Conversation item to check
675
- * @returns the type of channel
676
- */
677
- const getMessageType = (item) => {
678
- if (item.type === "GroupConversation" && item.name.toLowerCase().includes("other")) return {
679
- isChannel: false,
680
- type: "group-direct-message"
681
- };
682
- if (item.broadcasting) return {
683
- isChannel: true,
684
- type: "broadcast"
685
- };
686
- if (item.type === "GroupConversation") return {
687
- isChannel: true,
688
- type: "private-channel"
689
- };
690
- if (item.type === "DirectConversation") return {
691
- isChannel: false,
692
- type: "direct-message"
693
- };
694
- if (item.type === "EmailConversation") return {
695
- isChannel: false,
696
- type: "email"
697
- };
698
- if (item.type === "SmsConversation") return {
699
- isChannel: false,
700
- type: "sms"
701
- };
702
- if (item.type === "ChannelConversation") return {
703
- isChannel: true,
704
- type: "public-channel"
705
- };
706
- if (item.type === "BotConversation") return {
707
- isChannel: false,
708
- type: "bot"
709
- };
710
- if (item.type === "DownlineConversation") return {
711
- isChannel: true,
712
- type: "downline"
713
- };
714
- throw new Error("Invalid conversation type");
715
- };
716
- const getUserInitials = (conversation, user) => {
717
- if (user.phone === conversation.name) return "#";
718
- if (user.email === conversation.name) return "@";
719
- return [user.first_name?.charAt(0), user.last_name?.charAt(0)].filter(Boolean).join("") || "?";
720
- };
721
- const getFileTypeFromMimetype = (mimetype) => {
722
- if (mimetype.startsWith("image/")) return "image";
723
- if (mimetype.startsWith("video/")) return "video";
724
- if (mimetype === "application/pdf") return "pdf";
725
- if (mimetype.includes("spreadsheet") || mimetype.includes("excel") || mimetype.includes("sheet") || mimetype === "text/csv") return "sheets";
726
- if (mimetype.includes("powerpoint") || mimetype.includes("presentation")) return "powerpoint";
727
- if (mimetype.includes("document") || mimetype.includes("word") || mimetype.includes("rtf")) return "docx";
728
- if (mimetype.startsWith("text/")) return "txt";
729
- if (mimetype.includes("zip") || mimetype.includes("compressed") || mimetype.includes("archive") || mimetype.includes("tar") || mimetype.includes("gzip")) return "compressed";
730
- if (mimetype.startsWith("audio/")) return "audio";
731
- return "unknown";
732
- };
733
- //#endregion
734
- //#region ../../messaging/core/src/utils/string-utils.ts
735
- /**
736
- * Extracts the leading emoji from a string.
737
- * @param text The string to process.
738
- * @returns An object containing the emoji and the remaining text.
739
- */
740
- function extractEmoji$1(text) {
741
- const match = text.match(/^\p{Emoji_Presentation}/u);
742
- if (match) {
743
- const emoji = match[0];
744
- return {
745
- emoji,
746
- text: text.substring(emoji.length).trimStart()
747
- };
748
- }
749
- return {
750
- emoji: null,
751
- text
752
- };
753
- }
754
- /**
755
- * Formats a message channel name by replacing spaces and underscores with hyphens,
756
- * converting to lowercase, and removing any non-alphanumeric characters except hyphens.
757
- * @param name - The original channel name
758
- * @return The formatted channel name
759
- */
760
- function formatMessageChannelName$1(name) {
761
- return name.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "");
762
- }
763
- const MAX_GROUP_NAME_LENGTH$1 = 30;
764
- function formatGroupDisplayName$1(names, maxLength = MAX_GROUP_NAME_LENGTH$1) {
765
- const fullDisplayName = names.join(", ");
766
- if (fullDisplayName.length <= maxLength) return { displayName: fullDisplayName };
767
- for (let i = 0; i < names.length; i++) {
768
- const currentNames = names.slice(0, i + 1);
769
- const remainingCount = names.length - currentNames.length;
770
- if (remainingCount > 0) {
771
- const suffix = `+ ${remainingCount} other${remainingCount > 1 ? "s" : ""}`;
772
- if (`${currentNames.join(", ")} ${suffix}`.length > maxLength && i > 0) {
773
- const namesToShow = names.slice(0, i);
774
- const othersCount = names.length - i;
775
- const othersSuffix = `+${othersCount} other${othersCount > 1 ? "s" : ""}`;
776
- const truncatedDisplayName = namesToShow.join(", ");
777
- if (othersCount === 1) {
778
- const nameWithOneMore = names.slice(0, i + 1).join(", ");
779
- if (nameWithOneMore.length < truncatedDisplayName.length) return { displayName: nameWithOneMore };
780
- }
781
- return {
782
- displayName: truncatedDisplayName,
783
- remainder: othersSuffix
784
- };
785
- }
786
- }
787
- }
788
- const firstName = names[0] || "";
789
- const othersCount = names.length - 1;
790
- if (othersCount > 0) return {
791
- displayName: firstName,
792
- remainder: `+ ${othersCount} other${othersCount > 1 ? "s" : ""}`
793
- };
794
- return { displayName: firstName };
795
- }
796
- //#endregion
797
- //#region ../../messaging/core/src/utils/file-kinds.ts
798
- const ServerFileKindEnum$1 = zod.z.enum([
799
- "image",
800
- "video",
801
- "pdf",
802
- "sheets",
803
- "powerpoint",
804
- "docx"
805
- ]);
806
- const fallbackFileType$1 = "docx";
807
- //#endregion
808
72
  //#region ../../messaging/api-client/src/client.ts
809
73
  /**
810
74
  * Messaging-specific API error, extends the shared ApiError from api-client-core.
@@ -1094,7 +358,7 @@ const WithContactRecipientSchema = zod.z.strictObject({
1094
358
  });
1095
359
  createPaginatedSchema(RecipientSchema);
1096
360
  createPaginatedSchema(ReducedRecipientSchema);
1097
- zod.z.strictObject({ meta: zod.z.strictObject({ count: zod.z.number() }) });
361
+ const RecipientSearchCountSchema = zod.z.strictObject({ meta: zod.z.strictObject({ count: zod.z.number() }) });
1098
362
  //#endregion
1099
363
  //#region ../../messaging/api-client/src/schemas/drafts-scheduled.schema.ts
1100
364
  const DraftMessageSchema = zod.z.strictObject({
@@ -1240,7 +504,7 @@ zod.z.strictObject({ success: zod.z.boolean() }).or(zod.z.strictObject({
1240
504
  *
1241
505
  * These schemas normalize the slightly different index vs show/update shapes.
1242
506
  */
1243
- const ReactionStatsSchema = zod.z.record(zod.z.string(), zod.z.number());
507
+ const ReactionStatsSchema$1 = zod.z.record(zod.z.string(), zod.z.number());
1244
508
  const MessageCoreSchema = zod.z.strictObject({
1245
509
  id: zod.z.number(),
1246
510
  body: zod.z.string(),
@@ -1252,7 +516,7 @@ const MessageCoreSchema = zod.z.strictObject({
1252
516
  conversation_id: zod.z.number(),
1253
517
  status: zod.z.string(),
1254
518
  attachments: zod.z.array(AttachmentSchema),
1255
- reaction_stats: ReactionStatsSchema.nullable(),
519
+ reaction_stats: ReactionStatsSchema$1.nullable(),
1256
520
  reaction: zod.z.string().nullable().optional(),
1257
521
  metadata: MessageMetadataSchema.optional(),
1258
522
  emoji_only_count: zod.z.number().optional(),
@@ -1266,9 +530,9 @@ const MessageCoreSchema = zod.z.strictObject({
1266
530
  seen_by_count: zod.z.unknown()
1267
531
  });
1268
532
  const MessageIndexItemSchema = MessageCoreSchema;
1269
- const MessageItemSchema = MessageCoreSchema;
533
+ const MessageItemSchema$1 = MessageCoreSchema;
1270
534
  const MessageIndexResponseSchema = createPaginatedSchema(MessageIndexItemSchema);
1271
- const MessageListResponseSchema = createPaginatedSchema(MessageItemSchema);
535
+ const MessageListResponseSchema = createPaginatedSchema(MessageItemSchema$1);
1272
536
  const MessagePinResponseSchema = zod.z.strictObject({
1273
537
  message: zod.z.string(),
1274
538
  pinned: zod.z.boolean()
@@ -1321,328 +585,1117 @@ async function listMessages(config, conversation_id, input) {
1321
585
  endpoint: `/v1/messaging/conversations/${conversation_id}/messages`,
1322
586
  method: "GET",
1323
587
  input,
1324
- outputSchema: MessageIndexResponseSchema
588
+ outputSchema: MessageIndexResponseSchema
589
+ });
590
+ }
591
+ const _createMessageInputSchema = zod.z.strictObject({
592
+ message: zod.z.strictObject({
593
+ body: zod.z.string().optional(),
594
+ theme: zod.z.string().optional(),
595
+ subject: zod.z.string().optional(),
596
+ type: zod.z.string().optional(),
597
+ attachments_attributes: zod.z.array(AttachmentInputSchema).optional()
598
+ }),
599
+ client_message_id: zod.z.union([zod.z.string(), zod.z.number()]).optional()
600
+ });
601
+ const createMessageInputSchema = _createMessageInputSchema;
602
+ async function createMessage(config, conversation_id, input) {
603
+ return fetchMessaging(config, {
604
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages`,
605
+ method: "POST",
606
+ input,
607
+ inputSchema: createMessageInputSchema,
608
+ outputSchema: MessageItemSchema$1
609
+ });
610
+ }
611
+ /**
612
+ * POST /api/v1/messaging/conversations/:conversation_id/messages/reply
613
+ * Reply to an existing message by id; thread is captured in replied_to_id.
614
+ */
615
+ async function replyToMessage(config, conversation_id, message_id, input) {
616
+ return fetchMessaging(config, {
617
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/reply`,
618
+ method: "POST",
619
+ input: {
620
+ ...input,
621
+ message_id
622
+ },
623
+ inputSchema: _createMessageInputSchema.extend({ message_id: zod.z.number() }),
624
+ outputSchema: MessageItemSchema$1
625
+ });
626
+ }
627
+ zod.z.strictObject({ message: zod.z.strictObject({
628
+ body: zod.z.string().optional(),
629
+ theme: zod.z.string().optional()
630
+ }) });
631
+ /**
632
+ * POST /api/v1/messaging/conversations/:conversation_id/messages/:message_id/react.json
633
+ * React to a message with an emoji.
634
+ */
635
+ async function reactToMessage(config, conversation_id, input) {
636
+ const { message_id, reaction } = input;
637
+ return fetchMessaging(config, {
638
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${message_id}/react.json?reaction=${encodeURIComponent(reaction)}`,
639
+ method: "POST",
640
+ outputSchema: zod.z.string()
641
+ });
642
+ }
643
+ /**
644
+ * DELETE /api/v1/messaging/conversations/:conversation_id/messages/:message_id/unreact.json
645
+ * Remove the current recipient's reaction.
646
+ */
647
+ async function unreactToMessage(config, conversation_id, input) {
648
+ const { message_id } = input;
649
+ return fetchMessaging(config, {
650
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${message_id}/unreact.json`,
651
+ method: "DELETE",
652
+ outputSchema: zod.z.string()
653
+ });
654
+ }
655
+ /**
656
+ * GET /api/v1/messaging/conversations/:conversation_id/messages/pinned
657
+ * List pinned messages in the conversation.
658
+ */
659
+ async function listPinnedMessages(config, conversation_id, input) {
660
+ return fetchMessaging(config, {
661
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/pinned`,
662
+ method: "GET",
663
+ input,
664
+ outputSchema: MessageListResponseSchema
665
+ });
666
+ }
667
+ /**
668
+ * PUT /api/v1/messaging/conversations/:conversation_id/messages/:id/pin
669
+ * Pin a message (current recipient).
670
+ */
671
+ async function pinMessage(config, conversation_id, id) {
672
+ return fetchMessaging(config, {
673
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${id}/pin`,
674
+ method: "PUT",
675
+ outputSchema: MessagePinResponseSchema
676
+ });
677
+ }
678
+ /**
679
+ * PUT /api/v1/messaging/conversations/:conversation_id/messages/:id/unpin
680
+ * Unpin a message (current recipient).
681
+ */
682
+ async function unpinMessage(config, conversation_id, id) {
683
+ return fetchMessaging(config, {
684
+ endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${id}/unpin`,
685
+ method: "PUT",
686
+ outputSchema: MessagePinResponseSchema
687
+ });
688
+ }
689
+ //#endregion
690
+ //#region ../../messaging/api-client/src/api/conversations.api.ts
691
+ /**
692
+ * Conversations API (v1 messaging)
693
+ *
694
+ * Thin, fully-typed wrappers around the Rails messaging controllers.
695
+ * All functions validate inputs/outputs with zod and use `fetchMessaging`.
696
+ */
697
+ /**
698
+ * GET /api/v1/messaging/announcement_channel
699
+ */
700
+ async function getAnnouncementChannel(config) {
701
+ return fetchMessaging(config, {
702
+ endpoint: "/v1/messaging/announcement_channel",
703
+ method: "GET",
704
+ outputSchema: ConversationResponseSchema
705
+ });
706
+ }
707
+ async function listConversations(config, input) {
708
+ return fetchMessaging(config, {
709
+ endpoint: "/v1/messaging/conversations",
710
+ method: "GET",
711
+ input,
712
+ outputSchema: ConversationListResponseSchema
713
+ });
714
+ }
715
+ /**
716
+ * GET /api/v1/messaging/conversations/search
717
+ */
718
+ async function searchConversations(config, input) {
719
+ return fetchMessaging(config, {
720
+ endpoint: "/v1/messaging/conversations/search",
721
+ method: "GET",
722
+ input,
723
+ outputSchema: NominalConversationsResponseSchema
724
+ });
725
+ }
726
+ /**
727
+ * GET /api/v1/messaging/conversations/find
728
+ */
729
+ async function findConversation(config, input) {
730
+ return fetchMessaging(config, {
731
+ endpoint: "/v1/messaging/conversations/find",
732
+ method: "GET",
733
+ input,
734
+ outputSchema: ConversationResponseSchema
735
+ });
736
+ }
737
+ /**
738
+ * GET /api/v1/messaging/conversations/:id
739
+ */
740
+ async function getConversation(config, id) {
741
+ return fetchMessaging(config, {
742
+ endpoint: `/v1/messaging/conversations/${id}`,
743
+ method: "GET",
744
+ outputSchema: ConversationResponseSchema
745
+ });
746
+ }
747
+ const conversationCreateInputSchema = zod.z.strictObject({
748
+ conversation: zod.z.strictObject({
749
+ kind: zod.z.string().optional(),
750
+ type: zod.z.string().optional(),
751
+ name: zod.z.string().optional(),
752
+ description: zod.z.string().nullable().optional(),
753
+ broadcasting: zod.z.boolean().optional(),
754
+ filter: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
755
+ auto_add_recipients: zod.z.boolean().optional()
756
+ }),
757
+ recipients: zod.z.array(zod.z.strictObject({
758
+ id: zod.z.number().optional(),
759
+ uid: zod.z.number().optional(),
760
+ email: zod.z.string().email().optional(),
761
+ phone: zod.z.string().optional(),
762
+ first_name: zod.z.string().optional(),
763
+ last_name: zod.z.string().optional(),
764
+ image_url: zod.z.string().url().optional()
765
+ })).optional()
766
+ });
767
+ async function createConversation(config, input) {
768
+ return fetchMessaging(config, {
769
+ endpoint: "/v1/messaging/conversations",
770
+ method: "POST",
771
+ input,
772
+ inputSchema: conversationCreateInputSchema,
773
+ outputSchema: ConversationResponseSchema
774
+ });
775
+ }
776
+ zod.z.strictObject({ conversation: zod.z.strictObject({
777
+ name: zod.z.string().optional(),
778
+ description: zod.z.string().nullable().optional(),
779
+ additional_recipient_params: zod.z.strictObject({
780
+ receivable_id: zod.z.number(),
781
+ receivable_type: zod.z.string()
782
+ }).optional(),
783
+ auto_add_recipients: zod.z.boolean().optional()
784
+ }) });
785
+ //#endregion
786
+ //#region ../../messaging/api-client/src/api/recipients.api.ts
787
+ /**
788
+ * POST /api/v1/messaging/recipients/search
789
+ */
790
+ async function searchRecipientsCount(config, input) {
791
+ return fetchMessaging(config, {
792
+ endpoint: "/v1/messaging/recipients/search",
793
+ method: "POST",
794
+ input,
795
+ outputSchema: RecipientSearchCountSchema
796
+ });
797
+ }
798
+ /**
799
+ * GET /api/v1/messaging/conversations/connected_recipients
800
+ */
801
+ async function listConnectedRecipients(config, input) {
802
+ return fetchMessaging(config, {
803
+ endpoint: "/v1/messaging/conversations/connected_recipients",
804
+ method: "GET",
805
+ input,
806
+ outputSchema: createPaginatedSchema(input?.kind && [
807
+ "external",
808
+ "sms",
809
+ "email"
810
+ ].includes(input.kind) ? WithContactRecipientSchema : RecipientSchema)
811
+ });
812
+ }
813
+ //#endregion
814
+ //#region ../../messaging/api-client/src/api/drafts-scheduled.api.ts
815
+ const saveDraftInputSchema = zod.z.strictObject({ draft_message: zod.z.strictObject({
816
+ body: zod.z.string().nullable().optional(),
817
+ replied_to_id: zod.z.number().nullable().optional(),
818
+ attachments_attributes: zod.z.array(zod.z.strictObject({
819
+ url: zod.z.string().url(),
820
+ filename: zod.z.string(),
821
+ kind: zod.z.string(),
822
+ content_type: zod.z.string().optional(),
823
+ content_size: zod.z.number().optional(),
824
+ metadata: AttachmentInputMetadataSchema,
825
+ id: zod.z.number().optional(),
826
+ _destroy: zod.z.boolean().optional()
827
+ })).optional()
828
+ }) });
829
+ async function saveDraft(config, conversation_id, input) {
830
+ return fetchMessaging(config, {
831
+ endpoint: `/v1/messaging/conversations/${conversation_id}/save_draft`,
832
+ method: "PUT",
833
+ input,
834
+ inputSchema: saveDraftInputSchema,
835
+ outputSchema: DraftMessageSchema.or(zod.z.strictObject({}))
836
+ });
837
+ }
838
+ async function destroyDraft(config, conversation_id) {
839
+ return fetchMessaging(config, {
840
+ endpoint: `/v1/messaging/conversations/${conversation_id}/destroy_draft`,
841
+ method: "DELETE",
842
+ outputSchema: zod.z.unknown()
843
+ });
844
+ }
845
+ /**
846
+ * Scheduled Messages: per conversation
847
+ */
848
+ async function listScheduledMessages(config, conversation_id, input) {
849
+ return fetchMessaging(config, {
850
+ endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages`,
851
+ method: "GET",
852
+ input,
853
+ outputSchema: MessageListResponseSchema
854
+ });
855
+ }
856
+ const scheduledMessageInputSchema = zod.z.strictObject({ scheduled_message: zod.z.strictObject({
857
+ body: zod.z.string().optional(),
858
+ theme: zod.z.string().optional(),
859
+ scheduled_at: zod.z.string().datetime({ offset: true }),
860
+ attachments_attributes: zod.z.array(AttachmentInputSchema).optional(),
861
+ type: zod.z.string().optional()
862
+ }) });
863
+ async function createScheduledMessage(config, conversation_id, input) {
864
+ return fetchMessaging(config, {
865
+ endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages`,
866
+ method: "POST",
867
+ input,
868
+ inputSchema: scheduledMessageInputSchema,
869
+ outputSchema: MessageItemSchema$1
1325
870
  });
1326
871
  }
1327
- const _createMessageInputSchema = zod.z.strictObject({
1328
- message: zod.z.strictObject({
1329
- body: zod.z.string().optional(),
1330
- theme: zod.z.string().optional(),
1331
- subject: zod.z.string().optional(),
1332
- type: zod.z.string().optional(),
1333
- attachments_attributes: zod.z.array(AttachmentInputSchema).optional()
1334
- }),
1335
- client_message_id: zod.z.union([zod.z.string(), zod.z.number()]).optional()
1336
- });
1337
- const createMessageInputSchema = _createMessageInputSchema;
1338
- async function createMessage(config, conversation_id, input) {
872
+ async function updateScheduledMessage(config, conversation_id, id, input) {
1339
873
  return fetchMessaging(config, {
1340
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages`,
1341
- method: "POST",
874
+ endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages/${id}`,
875
+ method: "PUT",
1342
876
  input,
1343
- inputSchema: createMessageInputSchema,
1344
- outputSchema: MessageItemSchema
877
+ inputSchema: scheduledMessageInputSchema,
878
+ outputSchema: MessageItemSchema$1
879
+ });
880
+ }
881
+ async function destroyScheduledMessage(config, conversation_id, id) {
882
+ return fetchMessaging(config, {
883
+ endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages/${id}`,
884
+ method: "DELETE",
885
+ outputSchema: zod.z.strictObject({
886
+ success: zod.z.boolean(),
887
+ message: zod.z.string()
888
+ })
1345
889
  });
1346
890
  }
1347
891
  /**
1348
- * POST /api/v1/messaging/conversations/:conversation_id/messages/reply
1349
- * Reply to an existing message by id; thread is captured in replied_to_id.
892
+ * My Scheduled Messages
1350
893
  */
1351
- async function replyToMessage(config, conversation_id, message_id, input) {
894
+ async function listMyScheduledMessages(config, input) {
1352
895
  return fetchMessaging(config, {
1353
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/reply`,
1354
- method: "POST",
1355
- input: {
1356
- ...input,
1357
- message_id
1358
- },
1359
- inputSchema: _createMessageInputSchema.extend({ message_id: zod.z.number() }),
1360
- outputSchema: MessageItemSchema
896
+ endpoint: `/v1/messaging/my_scheduled_messages`,
897
+ method: "GET",
898
+ input,
899
+ outputSchema: DraftMessageListResponseSchema
1361
900
  });
1362
901
  }
1363
- zod.z.strictObject({ message: zod.z.strictObject({
1364
- body: zod.z.string().optional(),
1365
- theme: zod.z.string().optional()
902
+ zod.z.strictObject({ bot: zod.z.strictObject({
903
+ title: zod.z.string(),
904
+ description: zod.z.string().nullable().optional(),
905
+ external_uri: zod.z.string().url().nullable().optional(),
906
+ image_url: zod.z.string().url().nullable().optional(),
907
+ type: zod.z.string().optional(),
908
+ avatar_background_color: zod.z.string().nullable().optional(),
909
+ avatar_emoji: zod.z.string().nullable().optional(),
910
+ webhook_external_uri: zod.z.string().url().nullable().optional(),
911
+ webhook_auth_token: zod.z.string().nullable().optional(),
912
+ webhook_http_method: zod.z.string().nullable().optional(),
913
+ conversation_ids: zod.z.array(zod.z.number()).optional()
1366
914
  }) });
915
+ //#endregion
916
+ //#region src/messaging/create-messaging-api.ts
1367
917
  /**
1368
- * POST /api/v1/messaging/conversations/:conversation_id/messages/:message_id/react.json
1369
- * React to a message with an emoji.
918
+ * Creates a {@link MessagingApi} adapter backed by the messaging API client.
919
+ * This is the portal's adapter it wires the concrete client to the
920
+ * interface that messaging-core hooks consume via context.
1370
921
  */
1371
- async function reactToMessage(config, conversation_id, input) {
1372
- const { message_id, reaction } = input;
1373
- return fetchMessaging(config, {
1374
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${message_id}/react.json?reaction=${encodeURIComponent(reaction)}`,
1375
- method: "POST",
1376
- outputSchema: zod.z.string()
922
+ function createMessagingApi(config) {
923
+ return {
924
+ listConversations: (input) => listConversations(config, input),
925
+ getConversation: (id) => getConversation(config, id),
926
+ findConversation: (input) => findConversation(config, input),
927
+ createConversation: (input) => createConversation(config, input),
928
+ getAnnouncementChannel: () => getAnnouncementChannel(config),
929
+ listMessages: (cid, input) => listMessages(config, cid, input),
930
+ createMessage: (cid, input) => createMessage(config, cid, input),
931
+ replyToMessage: (cid, mid, input) => replyToMessage(config, cid, mid, input),
932
+ reactToMessage: (cid, input) => reactToMessage(config, cid, input),
933
+ unreactToMessage: (cid, input) => unreactToMessage(config, cid, input),
934
+ pinMessage: (cid, mid) => pinMessage(config, cid, mid),
935
+ unpinMessage: (cid, mid) => unpinMessage(config, cid, mid),
936
+ listPinnedMessages: (cid, input) => listPinnedMessages(config, cid, input),
937
+ listScheduledMessages: (cid, input) => listScheduledMessages(config, cid, input),
938
+ createScheduledMessage: (cid, input) => createScheduledMessage(config, cid, input),
939
+ updateScheduledMessage: (cid, id, input) => updateScheduledMessage(config, cid, id, input),
940
+ destroyScheduledMessage: (cid, id) => destroyScheduledMessage(config, cid, id),
941
+ listMyScheduledMessages: (input) => listMyScheduledMessages(config, input),
942
+ saveDraft: (cid, input) => saveDraft(config, cid, input),
943
+ destroyDraft: (cid) => destroyDraft(config, cid),
944
+ searchRecipientsCount: (input) => searchRecipientsCount(config, input),
945
+ listConnectedRecipients: (input) => listConnectedRecipients(config, input)
946
+ };
947
+ }
948
+ //#endregion
949
+ //#region src/messaging/use-messaging-config.ts
950
+ /**
951
+ * Hook that derives a MessagingApi adapter from the portal SDK's FluidProvider context.
952
+ *
953
+ * Maps FluidSDKConfig fields to a MessagingApiConfig, then wraps the
954
+ * concrete API client functions into a MessagingApi interface instance
955
+ * that messaging-core hooks consume via context.
956
+ */
957
+ function deriveWebsocketUrl(baseUrl) {
958
+ return `${baseUrl.replace(/\/+$/, "").replace(/\/api$/, "")}/cable`;
959
+ }
960
+ function useMessagingConfig() {
961
+ const { config } = require_FluidProvider.useFluidContext();
962
+ const auth = require_FluidProvider.useFluidAuthContext();
963
+ const getHeaders = (0, react.useCallback)(async () => {
964
+ const headers = {
965
+ "Content-Type": "application/json",
966
+ ...config.defaultHeaders
967
+ };
968
+ if (config.getAuthToken) {
969
+ const token = await config.getAuthToken();
970
+ if (token) headers.Authorization = `Bearer ${token}`;
971
+ }
972
+ return headers;
973
+ }, [config]);
974
+ const apiBaseUrl = (0, react.useMemo)(() => {
975
+ const base = config.baseUrl.replace(/\/+$/, "");
976
+ return base.endsWith("/api") ? base : `${base}/api`;
977
+ }, [config.baseUrl]);
978
+ const apiConfig = (0, react.useMemo)(() => ({
979
+ baseUrl: apiBaseUrl,
980
+ getHeaders,
981
+ ...config.onAuthError != null && { onAuthError: config.onAuthError }
982
+ }), [
983
+ apiBaseUrl,
984
+ config.onAuthError,
985
+ getHeaders
986
+ ]);
987
+ return {
988
+ apiConfig,
989
+ messagingApi: (0, react.useMemo)(() => createMessagingApi(apiConfig), [apiConfig]),
990
+ websocketUrl: (0, react.useMemo)(() => config.websocketUrl ?? deriveWebsocketUrl(config.baseUrl), [config.websocketUrl, config.baseUrl]),
991
+ token: auth.token
992
+ };
993
+ }
994
+ //#endregion
995
+ //#region ../../messaging/core/src/query-keys.ts
996
+ /**
997
+ * Query key factory for all messaging-related TanStack Query queries.
998
+ * Ensures consistent key shapes across hooks and cache helpers.
999
+ */
1000
+ const MESSAGES_QUERY_KEYS = {
1001
+ all: () => ["messages"],
1002
+ conversationMetadata: (conversationId) => [
1003
+ ...MESSAGES_QUERY_KEYS.all(),
1004
+ "conversation",
1005
+ conversationId
1006
+ ],
1007
+ newMessageFlag: (conversationId) => [
1008
+ ...MESSAGES_QUERY_KEYS.all(),
1009
+ "conversation",
1010
+ conversationId,
1011
+ "new_message_flag"
1012
+ ],
1013
+ listConversations: () => [...MESSAGES_QUERY_KEYS.all(), "conversations"],
1014
+ listMessages: (conversationId, messageId, startAtISO) => [
1015
+ ...MESSAGES_QUERY_KEYS.all(),
1016
+ "conversation",
1017
+ conversationId,
1018
+ "messages",
1019
+ messageId ? { startFromMessage: messageId } : startAtISO ? { startFromDate: startAtISO } : { startFromBottom: true }
1020
+ ],
1021
+ listPinnedMessages: (conversationId) => [
1022
+ ...MESSAGES_QUERY_KEYS.all(),
1023
+ "conversation",
1024
+ conversationId,
1025
+ "pinned"
1026
+ ],
1027
+ outbox: (conversationId) => [
1028
+ ...MESSAGES_QUERY_KEYS.all(),
1029
+ "conversation",
1030
+ conversationId,
1031
+ "outbox"
1032
+ ],
1033
+ listScheduledMessages: (conversationId) => [
1034
+ ...MESSAGES_QUERY_KEYS.all(),
1035
+ "conversation",
1036
+ conversationId,
1037
+ "scheduled"
1038
+ ],
1039
+ listMyScheduledMessages: () => [
1040
+ ...MESSAGES_QUERY_KEYS.all(),
1041
+ "scheduled",
1042
+ "mine"
1043
+ ],
1044
+ draft: (conversationId) => [
1045
+ ...MESSAGES_QUERY_KEYS.all(),
1046
+ "draft",
1047
+ conversationId
1048
+ ],
1049
+ announcementChannel: () => [...MESSAGES_QUERY_KEYS.all(), "announcement-channel"],
1050
+ downline: () => [...MESSAGES_QUERY_KEYS.all(), "downline"]
1051
+ };
1052
+ //#endregion
1053
+ //#region ../../messaging/core/src/constants.ts
1054
+ const WEBSOCKET_QUERY_OPTIONS = {
1055
+ staleTime: Infinity,
1056
+ refetchOnMount: "always",
1057
+ refetchOnReconnect: "always",
1058
+ refetchOnWindowFocus: false
1059
+ };
1060
+ /**
1061
+ * Scheduled messages are not websocket-driven; poll to keep fresh.
1062
+ */
1063
+ const SCHEDULED_MESSAGES_STALE_TIME = 6e4;
1064
+ const SCHEDULED_MESSAGES_REFETCH_INTERVAL = 6e4;
1065
+ /**
1066
+ * Pinned messages are not websocket-driven; poll to keep fresh.
1067
+ */
1068
+ const PINNED_MESSAGES_STALE_TIME = 6e4;
1069
+ const PINNED_MESSAGES_REFETCH_INTERVAL = 6e4;
1070
+ //#endregion
1071
+ //#region ../../messaging/core/src/cache-helpers.ts
1072
+ /**
1073
+ * seed conversation metadata caches from any conversations list queries.
1074
+ * returns the matched conversation if conversationId is provided and present in any list cache.
1075
+ */
1076
+ function seedConversationMetadataFromLists(queryClient, conversationId) {
1077
+ let matched;
1078
+ const seedFromData = (data) => {
1079
+ const pages = data?.pages ?? [];
1080
+ for (const page of pages) {
1081
+ const items = page?.[1]?.items ?? [];
1082
+ for (const item of items) {
1083
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.conversationMetadata(item.id), item);
1084
+ if (!matched && typeof conversationId === "number" && item.id === conversationId) matched = item;
1085
+ }
1086
+ }
1087
+ };
1088
+ seedFromData(queryClient.getQueryData(MESSAGES_QUERY_KEYS.listConversations()));
1089
+ const listQueries = queryClient.getQueryCache().findAll({ predicate: (q) => {
1090
+ const key = q.queryKey;
1091
+ return Array.isArray(key) && key.length >= 2 && key[0] === MESSAGES_QUERY_KEYS.all()[0] && key[1] === MESSAGES_QUERY_KEYS.listConversations()[1];
1092
+ } });
1093
+ for (const q of listQueries) seedFromData(queryClient.getQueryData(q.queryKey));
1094
+ return matched;
1095
+ }
1096
+ function isMessagesQueryKeyForConversation(queryKey, conversationId) {
1097
+ if (!Array.isArray(queryKey)) return false;
1098
+ return queryKey.length >= 4 && queryKey[0] === MESSAGES_QUERY_KEYS.all()[0] && queryKey[1] === "conversation" && queryKey[2] === conversationId && queryKey[3] === "messages";
1099
+ }
1100
+ function findMessageInCache(queryClient, conversationId, messageId) {
1101
+ const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) });
1102
+ for (const query of queries) {
1103
+ const data = queryClient.getQueryData(query.queryKey);
1104
+ if (data) for (const page of data.pages) {
1105
+ const message = page[1].items.find((m) => m.id === messageId);
1106
+ if (message) return message;
1107
+ }
1108
+ }
1109
+ }
1110
+ function forEachMessagesQuery(queryClient, conversationId, fn) {
1111
+ queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) }).forEach((q) => fn(q.queryKey));
1112
+ }
1113
+ function isBottomAnchorKey(queryKey) {
1114
+ if (!Array.isArray(queryKey)) return false;
1115
+ const anchor = queryKey[queryKey.length - 1];
1116
+ if (typeof anchor === "object" && anchor !== null) {
1117
+ if ("startFromBottom" in anchor) return anchor.startFromBottom === true;
1118
+ }
1119
+ return false;
1120
+ }
1121
+ function isPinnedQueryKeyForConversation(queryKey, conversationId) {
1122
+ if (!Array.isArray(queryKey)) return false;
1123
+ return queryKey.length >= 4 && queryKey[0] === MESSAGES_QUERY_KEYS.all()[0] && queryKey[1] === "conversation" && queryKey[2] === conversationId && queryKey[3] === "pinned";
1124
+ }
1125
+ function forEachPinnedQuery(queryClient, conversationId, fn) {
1126
+ queryClient.getQueryCache().findAll({ predicate: (q) => isPinnedQueryKeyForConversation(q.queryKey, conversationId) }).forEach((q) => fn(q.queryKey));
1127
+ }
1128
+ async function cancelMessageQueries(queryClient, conversationId) {
1129
+ const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isMessagesQueryKeyForConversation(q.queryKey, conversationId) });
1130
+ await Promise.all(queries.map((q) => queryClient.cancelQueries({ queryKey: q.queryKey })));
1131
+ }
1132
+ async function cancelPinnedQueries(queryClient, conversationId) {
1133
+ const queries = queryClient.getQueryCache().findAll({ predicate: (q) => isPinnedQueryKeyForConversation(q.queryKey, conversationId) });
1134
+ await Promise.all(queries.map((q) => queryClient.cancelQueries({ queryKey: q.queryKey })));
1135
+ }
1136
+ function applyMessagesOptimisticSend({ queryClient, conversationId, optimisticMessage }) {
1137
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1138
+ if (!isBottomAnchorKey(key)) return;
1139
+ queryClient.setQueryData(key, (oldData) => {
1140
+ const newOptimisticPage = [{ pagination: {
1141
+ current: 1,
1142
+ previous: null,
1143
+ next: null,
1144
+ per_page: oldData?.pages?.[0]?.[0]?.pagination?.per_page ?? 20,
1145
+ pages: oldData?.pages?.[0]?.[0]?.pagination?.pages ?? 1,
1146
+ count: oldData?.pages?.[0]?.[0]?.pagination?.count ?? 0
1147
+ } }, { items: [optimisticMessage] }];
1148
+ if (!oldData) return {
1149
+ pages: [newOptimisticPage],
1150
+ pageParams: [1]
1151
+ };
1152
+ const pages = oldData.pages.map((page, idx) => {
1153
+ if (idx === 0) {
1154
+ const currentItems = page?.[1]?.items ?? [];
1155
+ return [{ pagination: {
1156
+ ...page?.[0]?.pagination ?? {},
1157
+ count: page?.[0]?.pagination?.count ?? 0
1158
+ } }, { items: [optimisticMessage, ...currentItems] }];
1159
+ }
1160
+ return page;
1161
+ });
1162
+ return {
1163
+ ...oldData,
1164
+ pages
1165
+ };
1166
+ });
1167
+ });
1168
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
1169
+ const prev = old?.items ?? [];
1170
+ if (prev.some((m) => m.id === optimisticMessage.id)) return old;
1171
+ return { items: [...prev, optimisticMessage] };
1377
1172
  });
1378
1173
  }
1379
1174
  /**
1380
- * DELETE /api/v1/messaging/conversations/:conversation_id/messages/:message_id/unreact.json
1381
- * Remove the current recipient's reaction.
1175
+ * markNewMessageFlagForConversation
1176
+ *
1177
+ * sets a lightweight flag query for a conversation indicating a new message
1178
+ * arrived while the user may not be at the bottom. UI can reset it after
1179
+ * scrolling or showing a pill.
1382
1180
  */
1383
- async function unreactToMessage(config, conversation_id, input) {
1384
- const { message_id } = input;
1385
- return fetchMessaging(config, {
1386
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${message_id}/unreact.json`,
1387
- method: "DELETE",
1388
- outputSchema: zod.z.string()
1181
+ function markNewMessageFlagForConversation(queryClient, conversationId) {
1182
+ queryClient.setQueryData([
1183
+ MESSAGES_QUERY_KEYS.all()[0],
1184
+ "conversation",
1185
+ conversationId,
1186
+ "new_message_flag"
1187
+ ], true);
1188
+ }
1189
+ function clearNewMessageFlagForConversation(queryClient, conversationId) {
1190
+ queryClient.setQueryData([
1191
+ MESSAGES_QUERY_KEYS.all()[0],
1192
+ "conversation",
1193
+ conversationId,
1194
+ "new_message_flag"
1195
+ ], false);
1196
+ }
1197
+ function applyMessageUpsertToCache({ queryClient, conversationId, message, replaceOptimisticId, clientMessageId }) {
1198
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1199
+ queryClient.setQueryData(key, (oldData) => {
1200
+ if (!oldData?.pages?.length) {
1201
+ if (!isBottomAnchorKey(key)) return oldData;
1202
+ return {
1203
+ pages: [[{ pagination: {
1204
+ current: 1,
1205
+ previous: null,
1206
+ next: null,
1207
+ per_page: 20,
1208
+ pages: 1,
1209
+ count: 1
1210
+ } }, { items: [message] }]],
1211
+ pageParams: [1]
1212
+ };
1213
+ }
1214
+ const normalizedClientMessageId = (() => {
1215
+ if (typeof clientMessageId === "string" && clientMessageId.length > 0) return clientMessageId;
1216
+ const fromMeta = (() => {
1217
+ const meta = message.metadata;
1218
+ if (meta && typeof meta === "object" && "client_message_id" in meta) {
1219
+ const id = meta.client_message_id;
1220
+ if (typeof id === "string" || typeof id === "number" && Number.isFinite(id)) return id;
1221
+ }
1222
+ })();
1223
+ if (typeof fromMeta === "string" || typeof fromMeta === "number" && Number.isFinite(fromMeta)) return String(fromMeta);
1224
+ })();
1225
+ const finalMessage = (() => {
1226
+ if (!normalizedClientMessageId) return message;
1227
+ const currentMeta = message.metadata;
1228
+ if (currentMeta != null && typeof currentMeta === "object" && (typeof currentMeta.client_message_id === "string" || typeof currentMeta.client_message_id === "number")) return message;
1229
+ return {
1230
+ ...message,
1231
+ metadata: {
1232
+ ...currentMeta && typeof currentMeta === "object" ? currentMeta : {},
1233
+ client_message_id: normalizedClientMessageId
1234
+ }
1235
+ };
1236
+ })();
1237
+ let replaced = false;
1238
+ const pages = oldData.pages.map((page) => {
1239
+ const [meta, payload] = page;
1240
+ const items = payload.items.slice();
1241
+ const idxById = items.findIndex((m) => m.id === (replaceOptimisticId ?? message.id));
1242
+ if (idxById !== -1) {
1243
+ items[idxById] = finalMessage;
1244
+ replaced = true;
1245
+ return [meta, { items }];
1246
+ }
1247
+ if (normalizedClientMessageId) {
1248
+ const idxByClientId = items.findIndex((m) => typeof m?.metadata === "object" && m?.metadata != null && ((id) => id != null && String(id) === normalizedClientMessageId)(m.metadata?.client_message_id));
1249
+ if (idxByClientId !== -1) {
1250
+ items[idxByClientId] = finalMessage;
1251
+ replaced = true;
1252
+ return [meta, { items }];
1253
+ }
1254
+ }
1255
+ const idxExisting = items.findIndex((m) => m.id === message.id);
1256
+ if (idxExisting !== -1) {
1257
+ items[idxExisting] = finalMessage;
1258
+ replaced = true;
1259
+ return [meta, { items }];
1260
+ }
1261
+ return [meta, { items }];
1262
+ });
1263
+ if (!replaced) {
1264
+ if (!isBottomAnchorKey(key)) return oldData;
1265
+ const first = pages[0];
1266
+ const nextPages = pages.slice();
1267
+ const newFirstItems = [finalMessage, ...first?.[1]?.items ?? []];
1268
+ const newCount = (first?.[0]?.pagination?.count ?? 0) + 1;
1269
+ nextPages[0] = [{ pagination: {
1270
+ ...first?.[0]?.pagination ?? {},
1271
+ count: newCount
1272
+ } }, { items: newFirstItems }];
1273
+ return {
1274
+ ...oldData,
1275
+ pages: nextPages
1276
+ };
1277
+ }
1278
+ return {
1279
+ ...oldData,
1280
+ pages
1281
+ };
1282
+ });
1283
+ });
1284
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
1285
+ if (!old?.items) return old;
1286
+ return { items: old.items.filter((m) => m.id !== (replaceOptimisticId ?? -99999999)) };
1287
+ });
1288
+ }
1289
+ function applyMessageDeleteFromCache({ queryClient, conversationId, messageId }) {
1290
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1291
+ queryClient.setQueryData(key, (oldData) => {
1292
+ if (!oldData) return oldData;
1293
+ const pages = oldData.pages.map((page) => {
1294
+ const [meta, payload] = page;
1295
+ const items = payload.items.filter((m) => m.id !== messageId);
1296
+ const newCount = Math.max(0, (meta?.pagination?.count ?? 0) - (payload.items.length - items.length));
1297
+ return [{ pagination: {
1298
+ ...meta?.pagination ?? {},
1299
+ count: newCount
1300
+ } }, { items }];
1301
+ });
1302
+ return {
1303
+ ...oldData,
1304
+ pages
1305
+ };
1306
+ });
1307
+ });
1308
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.outbox(conversationId), (old) => {
1309
+ if (!old?.items) return old;
1310
+ return { items: old.items.filter((m) => m.id !== messageId) };
1311
+ });
1312
+ }
1313
+ function applyReactionUpdateToCache({ queryClient, conversationId, messageId, reaction, reaction_stats }) {
1314
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1315
+ queryClient.setQueryData(key, (oldData) => {
1316
+ if (!oldData) return oldData;
1317
+ const pages = oldData.pages.map((page) => {
1318
+ const [meta, payload] = page;
1319
+ return [meta, { items: payload.items.map((m) => m.id === messageId ? {
1320
+ ...m,
1321
+ reaction,
1322
+ reaction_stats
1323
+ } : m) }];
1324
+ });
1325
+ return {
1326
+ ...oldData,
1327
+ pages
1328
+ };
1329
+ });
1330
+ });
1331
+ forEachPinnedQuery(queryClient, conversationId, (key) => {
1332
+ queryClient.setQueryData(key, (oldData) => {
1333
+ if (!oldData) return oldData;
1334
+ const pages = oldData.pages.map((page) => {
1335
+ const [meta, payload] = page;
1336
+ return [meta, { items: payload.items.map((m) => m.id === messageId ? {
1337
+ ...m,
1338
+ reaction,
1339
+ reaction_stats
1340
+ } : m) }];
1341
+ });
1342
+ return {
1343
+ ...oldData,
1344
+ pages
1345
+ };
1346
+ });
1347
+ });
1348
+ }
1349
+ function applyReactionStatsToCache({ queryClient, conversationId, messageId, reaction_stats }) {
1350
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1351
+ queryClient.setQueryData(key, (oldData) => {
1352
+ if (!oldData) return;
1353
+ const pages = oldData.pages.map((page) => {
1354
+ const [meta, payload] = page;
1355
+ return [meta, { items: payload.items.map((m) => m.id === messageId ? {
1356
+ ...m,
1357
+ reaction_stats
1358
+ } : m) }];
1359
+ });
1360
+ return {
1361
+ ...oldData,
1362
+ pages
1363
+ };
1364
+ });
1365
+ });
1366
+ forEachPinnedQuery(queryClient, conversationId, (key) => {
1367
+ queryClient.setQueryData(key, (oldData) => {
1368
+ if (!oldData) return oldData;
1369
+ const pages = oldData.pages.map((page) => {
1370
+ const [meta, payload] = page;
1371
+ return [meta, { items: payload.items.map((m) => m.id === messageId ? {
1372
+ ...m,
1373
+ reaction_stats
1374
+ } : m) }];
1375
+ });
1376
+ return {
1377
+ ...oldData,
1378
+ pages
1379
+ };
1380
+ });
1381
+ });
1382
+ }
1383
+ function applyConversationMetaToCaches({ queryClient, conversation }) {
1384
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.conversationMetadata(conversation.id), (old) => {
1385
+ if (!old) return old;
1386
+ return {
1387
+ ...old,
1388
+ ...conversation
1389
+ };
1390
+ });
1391
+ const listQueries = queryClient.getQueryCache().findAll({ predicate: (q) => {
1392
+ const key = q.queryKey;
1393
+ return Array.isArray(key) && key.length >= 2 && key[0] === MESSAGES_QUERY_KEYS.all()[0] && key[1] === MESSAGES_QUERY_KEYS.listConversations()[1];
1394
+ } });
1395
+ for (const q of listQueries) queryClient.setQueryData(q.queryKey, (oldData) => {
1396
+ const data = oldData;
1397
+ if (!data?.pages) return oldData;
1398
+ return {
1399
+ ...data,
1400
+ pages: data.pages.map((page) => [page[0], {
1401
+ ...page[1],
1402
+ items: page[1].items.map((conv) => conv.id === conversation.id ? {
1403
+ ...conv,
1404
+ ...typeof conversation.unread === "boolean" ? { unread: conversation.unread } : {},
1405
+ ...typeof conversation.unread_messages_count === "number" ? { unread_messages_count: conversation.unread_messages_count } : {}
1406
+ } : conv)
1407
+ }])
1408
+ };
1389
1409
  });
1390
1410
  }
1391
- /**
1392
- * GET /api/v1/messaging/conversations/:conversation_id/messages/pinned
1393
- * List pinned messages in the conversation.
1394
- */
1395
- async function listPinnedMessages(config, conversation_id, input) {
1396
- return fetchMessaging(config, {
1397
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/pinned`,
1398
- method: "GET",
1399
- input,
1400
- outputSchema: MessageListResponseSchema
1411
+ function applyConversationDestroyed({ queryClient, conversationId }) {
1412
+ queryClient.removeQueries({ queryKey: MESSAGES_QUERY_KEYS.conversationMetadata(conversationId) });
1413
+ queryClient.setQueryData(MESSAGES_QUERY_KEYS.listConversations(), (oldData) => {
1414
+ const data = oldData;
1415
+ if (!data?.pages) return oldData;
1416
+ return {
1417
+ ...data,
1418
+ pages: data.pages.map((page) => [page[0], {
1419
+ ...page[1],
1420
+ items: page[1].items.filter((c) => c.id !== conversationId)
1421
+ }])
1422
+ };
1401
1423
  });
1402
1424
  }
1403
- /**
1404
- * PUT /api/v1/messaging/conversations/:conversation_id/messages/:id/pin
1405
- * Pin a message (current recipient).
1406
- */
1407
- async function pinMessage(config, conversation_id, id) {
1408
- return fetchMessaging(config, {
1409
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${id}/pin`,
1410
- method: "PUT",
1411
- outputSchema: MessagePinResponseSchema
1425
+ function applyMessagePinnedFlag({ queryClient, conversationId, messageId, pinned, message }) {
1426
+ forEachMessagesQuery(queryClient, conversationId, (key) => {
1427
+ queryClient.setQueryData(key, (oldData) => {
1428
+ if (!oldData) return oldData;
1429
+ const pages = oldData.pages.map((page) => {
1430
+ const [meta, payload] = page;
1431
+ return [meta, { items: payload.items.map((m) => m.id === messageId ? {
1432
+ ...m,
1433
+ pinned
1434
+ } : m) }];
1435
+ });
1436
+ return {
1437
+ ...oldData,
1438
+ pages
1439
+ };
1440
+ });
1412
1441
  });
1413
- }
1414
- /**
1415
- * PUT /api/v1/messaging/conversations/:conversation_id/messages/:id/unpin
1416
- * Unpin a message (current recipient).
1417
- */
1418
- async function unpinMessage(config, conversation_id, id) {
1419
- return fetchMessaging(config, {
1420
- endpoint: `/v1/messaging/conversations/${conversation_id}/messages/${id}/unpin`,
1421
- method: "PUT",
1422
- outputSchema: MessagePinResponseSchema
1442
+ forEachPinnedQuery(queryClient, conversationId, (key) => {
1443
+ queryClient.setQueryData(key, (oldData) => {
1444
+ if (!oldData) return oldData;
1445
+ const pages = oldData.pages.map((page, idx) => {
1446
+ const [meta, payload] = page;
1447
+ if (idx !== 0) return page;
1448
+ const exists = payload.items.some((m) => m.id === messageId);
1449
+ if (pinned) {
1450
+ if (exists) return page;
1451
+ const msg = message ?? payload.items.find((m) => m.id === messageId);
1452
+ if (!msg) return page;
1453
+ return [meta, { items: [msg, ...payload.items] }];
1454
+ }
1455
+ return [meta, { items: payload.items.filter((m) => m.id !== messageId) }];
1456
+ });
1457
+ return {
1458
+ ...oldData,
1459
+ pages
1460
+ };
1461
+ });
1423
1462
  });
1424
1463
  }
1425
1464
  //#endregion
1426
- //#region ../../messaging/api-client/src/api/conversations.api.ts
1427
- /**
1428
- * Conversations API (v1 messaging)
1429
- *
1430
- * Thin, fully-typed wrappers around the Rails messaging controllers.
1431
- * All functions validate inputs/outputs with zod and use `fetchMessaging`.
1432
- */
1465
+ //#region ../../messaging/core/src/utils/reaction-utils.ts
1433
1466
  /**
1434
- * GET /api/v1/messaging/announcement_channel
1467
+ * Splits an emoji string into an array of grapheme clusters (perceived characters/emojis).
1468
+ * Uses `Intl.Segmenter` when available for correct multi-codepoint emoji handling.
1469
+ * Falls back to `[...string]` (code-point iteration) on browsers without `Intl.Segmenter`
1470
+ * (e.g. Firefox < 119), which may incorrectly split ZWJ sequences, skin-tone modifiers,
1471
+ * and flag emojis into multiple entries.
1472
+ * @param emojiString The string of emojis to split.
1473
+ * @returns An array of strings, where each string is a single emoji.
1435
1474
  */
1436
- async function getAnnouncementChannel(config) {
1437
- return fetchMessaging(config, {
1438
- endpoint: "/v1/messaging/announcement_channel",
1439
- method: "GET",
1440
- outputSchema: ConversationResponseSchema
1441
- });
1442
- }
1443
- async function listConversations(config, input) {
1444
- return fetchMessaging(config, {
1445
- endpoint: "/v1/messaging/conversations",
1446
- method: "GET",
1447
- input,
1448
- outputSchema: ConversationListResponseSchema
1449
- });
1475
+ function splitEmojis(emojiString) {
1476
+ if (!emojiString) return [];
1477
+ if (typeof Intl !== "undefined" && typeof Intl.Segmenter === "function") {
1478
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
1479
+ return Array.from(segmenter.segment(emojiString)).map((segment) => segment.segment);
1480
+ }
1481
+ return [...emojiString];
1450
1482
  }
1451
1483
  /**
1452
- * GET /api/v1/messaging/conversations/search
1484
+ * Flattens reaction_stats to count individual emojis.
1485
+ * E.g., {"😀😂": 1, "👍": 2, "😀": 1} becomes {"😀": 2, "😂": 1, "👍": 2}
1486
+ * Handles multi-codepoint emojis correctly.
1487
+ * @param stats The raw reaction_stats object from the message.
1488
+ * @returns A record where keys are single emojis and values are their total counts.
1453
1489
  */
1454
- async function searchConversations(config, input) {
1455
- return fetchMessaging(config, {
1456
- endpoint: "/v1/messaging/conversations/search",
1457
- method: "GET",
1458
- input,
1459
- outputSchema: NominalConversationsResponseSchema
1460
- });
1490
+ function flattenReactionStats(stats) {
1491
+ if (!stats) return {};
1492
+ const flattened = {};
1493
+ for (const emojiKey in stats) {
1494
+ const count = stats[emojiKey];
1495
+ if (typeof count !== "number" || count === 0) continue;
1496
+ const individualEmojis = splitEmojis(emojiKey);
1497
+ for (const emoji of individualEmojis) flattened[emoji] = (flattened[emoji] || 0) + count;
1498
+ }
1499
+ return flattened;
1461
1500
  }
1462
1501
  /**
1463
- * GET /api/v1/messaging/conversations/find
1502
+ * Calculates the optimistic state for a message's reactions when a user's reaction changes.
1503
+ * @param currentState The current reaction parts of the message ({ reaction, reaction_stats }).
1504
+ * @param newUserReaction The user's new complete reaction string (e.g., "😀👍", "😂", or null to unreact).
1505
+ * @returns An object with the updated `reaction` and `reaction_stats`.
1464
1506
  */
1465
- async function findConversation(config, input) {
1466
- return fetchMessaging(config, {
1467
- endpoint: "/v1/messaging/conversations/find",
1468
- method: "GET",
1469
- input,
1470
- outputSchema: ConversationResponseSchema
1471
- });
1507
+ function calculateOptimisticReactionUpdate(currentState, newUserReaction) {
1508
+ const previousUserReaction = currentState.reaction;
1509
+ const newStats = { ...currentState.reaction_stats || {} };
1510
+ const finalNewUserReaction = newUserReaction === "" ? null : newUserReaction;
1511
+ if (previousUserReaction === finalNewUserReaction) return {
1512
+ reaction: previousUserReaction,
1513
+ reaction_stats: newStats
1514
+ };
1515
+ if (previousUserReaction && typeof newStats[previousUserReaction] === "number") newStats[previousUserReaction] = Math.max(0, newStats[previousUserReaction] - 1);
1516
+ if (finalNewUserReaction) newStats[finalNewUserReaction] = (typeof newStats[finalNewUserReaction] === "number" ? newStats[finalNewUserReaction] : 0) + 1;
1517
+ return {
1518
+ reaction: finalNewUserReaction,
1519
+ reaction_stats: newStats
1520
+ };
1472
1521
  }
1473
1522
  /**
1474
- * GET /api/v1/messaging/conversations/:id
1523
+ * Generates a unique, sorted string from an array of emoji characters.
1524
+ * Each string in the input array is assumed to be a single, correctly segmented emoji.
1525
+ * E.g., ["😂", "😀", "😂"] becomes "😀😂"
1526
+ * @param emojis An array of single emoji strings (grapheme clusters).
1527
+ * @returns A sorted string of unique emojis, or an empty string if no emojis are provided.
1475
1528
  */
1476
- async function getConversation(config, id) {
1477
- return fetchMessaging(config, {
1478
- endpoint: `/v1/messaging/conversations/${id}`,
1479
- method: "GET",
1480
- outputSchema: ConversationResponseSchema
1481
- });
1482
- }
1483
- const conversationCreateInputSchema = zod.z.strictObject({
1484
- conversation: zod.z.strictObject({
1485
- kind: zod.z.string().optional(),
1486
- type: zod.z.string().optional(),
1487
- name: zod.z.string().optional(),
1488
- description: zod.z.string().nullable().optional(),
1489
- broadcasting: zod.z.boolean().optional(),
1490
- filter: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
1491
- auto_add_recipients: zod.z.boolean().optional()
1492
- }),
1493
- recipients: zod.z.array(zod.z.strictObject({
1494
- id: zod.z.number().optional(),
1495
- uid: zod.z.number().optional(),
1496
- email: zod.z.string().email().optional(),
1497
- phone: zod.z.string().optional(),
1498
- first_name: zod.z.string().optional(),
1499
- last_name: zod.z.string().optional(),
1500
- image_url: zod.z.string().url().optional()
1501
- })).optional()
1502
- });
1503
- async function createConversation(config, input) {
1504
- return fetchMessaging(config, {
1505
- endpoint: "/v1/messaging/conversations",
1506
- method: "POST",
1507
- input,
1508
- inputSchema: conversationCreateInputSchema,
1509
- outputSchema: ConversationResponseSchema
1510
- });
1529
+ function generateSortedEmojiStringFromArray(emojis) {
1530
+ if (!emojis || emojis.length === 0) return "";
1531
+ return [...new Set(emojis)].sort((a, b) => a.localeCompare(b, "en")).join("");
1511
1532
  }
1512
- zod.z.strictObject({ conversation: zod.z.strictObject({
1513
- name: zod.z.string().optional(),
1514
- description: zod.z.string().nullable().optional(),
1515
- additional_recipient_params: zod.z.strictObject({
1516
- receivable_id: zod.z.number(),
1517
- receivable_type: zod.z.string()
1518
- }).optional(),
1519
- auto_add_recipients: zod.z.boolean().optional()
1520
- }) });
1521
1533
  //#endregion
1522
- //#region ../../messaging/api-client/src/api/recipients.api.ts
1534
+ //#region ../../messaging/core/src/utils/messaging.ts
1535
+ function mapConversationTypeToMessageType(conversationType) {
1536
+ switch (conversationType) {
1537
+ case "DirectConversation": return "DirectMessage";
1538
+ case "SmsConversation": return "SmsMessage";
1539
+ case "EmailConversation": return "EmailMessage";
1540
+ case "ChannelConversation":
1541
+ case "GroupConversation": return "DirectMessage";
1542
+ case "BotConversation": return "BotMessage";
1543
+ default: return "DirectMessage";
1544
+ }
1545
+ }
1523
1546
  /**
1524
- * GET /api/v1/messaging/conversations/connected_recipients
1547
+ * Determines the category of a chat. This is slightly different from the backend's
1548
+ * category field.
1549
+ *
1550
+ * @param item - Conversation item to check
1551
+ * @returns the type of channel
1525
1552
  */
1526
- async function listConnectedRecipients(config, input) {
1527
- return fetchMessaging(config, {
1528
- endpoint: "/v1/messaging/conversations/connected_recipients",
1529
- method: "GET",
1530
- input,
1531
- outputSchema: createPaginatedSchema(input?.kind && [
1532
- "external",
1533
- "sms",
1534
- "email"
1535
- ].includes(input.kind) ? WithContactRecipientSchema : RecipientSchema)
1536
- });
1537
- }
1553
+ const getMessageType = (item) => {
1554
+ if (item.type === "GroupConversation" && item.name.toLowerCase().includes("other")) return {
1555
+ isChannel: false,
1556
+ type: "group-direct-message"
1557
+ };
1558
+ if (item.broadcasting) return {
1559
+ isChannel: true,
1560
+ type: "broadcast"
1561
+ };
1562
+ if (item.type === "GroupConversation") return {
1563
+ isChannel: true,
1564
+ type: "private-channel"
1565
+ };
1566
+ if (item.type === "DirectConversation") return {
1567
+ isChannel: false,
1568
+ type: "direct-message"
1569
+ };
1570
+ if (item.type === "EmailConversation") return {
1571
+ isChannel: false,
1572
+ type: "email"
1573
+ };
1574
+ if (item.type === "SmsConversation") return {
1575
+ isChannel: false,
1576
+ type: "sms"
1577
+ };
1578
+ if (item.type === "ChannelConversation") return {
1579
+ isChannel: true,
1580
+ type: "public-channel"
1581
+ };
1582
+ if (item.type === "BotConversation") return {
1583
+ isChannel: false,
1584
+ type: "bot"
1585
+ };
1586
+ if (item.type === "DownlineConversation") return {
1587
+ isChannel: true,
1588
+ type: "downline"
1589
+ };
1590
+ throw new Error("Invalid conversation type");
1591
+ };
1592
+ const getUserInitials = (conversation, user) => {
1593
+ if (user.phone === conversation.name) return "#";
1594
+ if (user.email === conversation.name) return "@";
1595
+ return [user.first_name?.charAt(0), user.last_name?.charAt(0)].filter(Boolean).join("") || "?";
1596
+ };
1597
+ const getFileTypeFromMimetype = (mimetype) => {
1598
+ if (mimetype.startsWith("image/")) return "image";
1599
+ if (mimetype.startsWith("video/")) return "video";
1600
+ if (mimetype === "application/pdf") return "pdf";
1601
+ if (mimetype.includes("spreadsheet") || mimetype.includes("excel") || mimetype.includes("sheet") || mimetype === "text/csv") return "sheets";
1602
+ if (mimetype.includes("powerpoint") || mimetype.includes("presentation")) return "powerpoint";
1603
+ if (mimetype.includes("document") || mimetype.includes("word") || mimetype.includes("rtf")) return "docx";
1604
+ if (mimetype.startsWith("text/")) return "txt";
1605
+ if (mimetype.includes("zip") || mimetype.includes("compressed") || mimetype.includes("archive") || mimetype.includes("tar") || mimetype.includes("gzip")) return "compressed";
1606
+ if (mimetype.startsWith("audio/")) return "audio";
1607
+ return "unknown";
1608
+ };
1538
1609
  //#endregion
1539
- //#region ../../messaging/api-client/src/api/drafts-scheduled.api.ts
1540
- const saveDraftInputSchema = zod.z.strictObject({ draft_message: zod.z.strictObject({
1541
- body: zod.z.string().nullable().optional(),
1542
- replied_to_id: zod.z.number().nullable().optional(),
1543
- attachments_attributes: zod.z.array(zod.z.strictObject({
1544
- url: zod.z.string().url(),
1545
- filename: zod.z.string(),
1546
- kind: zod.z.string(),
1547
- content_type: zod.z.string().optional(),
1548
- content_size: zod.z.number().optional(),
1549
- metadata: AttachmentInputMetadataSchema,
1550
- id: zod.z.number().optional(),
1551
- _destroy: zod.z.boolean().optional()
1552
- })).optional()
1553
- }) });
1554
- async function saveDraft(config, conversation_id, input) {
1555
- return fetchMessaging(config, {
1556
- endpoint: `/v1/messaging/conversations/${conversation_id}/save_draft`,
1557
- method: "PUT",
1558
- input,
1559
- inputSchema: saveDraftInputSchema,
1560
- outputSchema: DraftMessageSchema.or(zod.z.strictObject({}))
1561
- });
1562
- }
1563
- async function destroyDraft(config, conversation_id) {
1564
- return fetchMessaging(config, {
1565
- endpoint: `/v1/messaging/conversations/${conversation_id}/destroy_draft`,
1566
- method: "DELETE",
1567
- outputSchema: zod.z.unknown()
1568
- });
1569
- }
1610
+ //#region ../../messaging/core/src/utils/string-utils.ts
1570
1611
  /**
1571
- * Scheduled Messages: per conversation
1612
+ * Extracts the leading emoji from a string.
1613
+ * @param text The string to process.
1614
+ * @returns An object containing the emoji and the remaining text.
1572
1615
  */
1573
- async function listScheduledMessages(config, conversation_id, input) {
1574
- return fetchMessaging(config, {
1575
- endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages`,
1576
- method: "GET",
1577
- input,
1578
- outputSchema: MessageListResponseSchema
1579
- });
1580
- }
1581
- const scheduledMessageInputSchema = zod.z.strictObject({ scheduled_message: zod.z.strictObject({
1582
- body: zod.z.string().optional(),
1583
- theme: zod.z.string().optional(),
1584
- scheduled_at: zod.z.string().datetime({ offset: true }),
1585
- attachments_attributes: zod.z.array(AttachmentInputSchema).optional(),
1586
- type: zod.z.string().optional()
1587
- }) });
1588
- async function createScheduledMessage(config, conversation_id, input) {
1589
- return fetchMessaging(config, {
1590
- endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages`,
1591
- method: "POST",
1592
- input,
1593
- inputSchema: scheduledMessageInputSchema,
1594
- outputSchema: MessageItemSchema
1595
- });
1596
- }
1597
- async function updateScheduledMessage(config, conversation_id, id, input) {
1598
- return fetchMessaging(config, {
1599
- endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages/${id}`,
1600
- method: "PUT",
1601
- input,
1602
- inputSchema: scheduledMessageInputSchema,
1603
- outputSchema: MessageItemSchema
1604
- });
1605
- }
1606
- async function destroyScheduledMessage(config, conversation_id, id) {
1607
- return fetchMessaging(config, {
1608
- endpoint: `/v1/messaging/conversations/${conversation_id}/scheduled_messages/${id}`,
1609
- method: "DELETE",
1610
- outputSchema: zod.z.strictObject({
1611
- success: zod.z.boolean(),
1612
- message: zod.z.string()
1613
- })
1614
- });
1616
+ function extractEmoji$1(text) {
1617
+ const match = text.match(/^\p{Emoji_Presentation}/u);
1618
+ if (match) {
1619
+ const emoji = match[0];
1620
+ return {
1621
+ emoji,
1622
+ text: text.substring(emoji.length).trimStart()
1623
+ };
1624
+ }
1625
+ return {
1626
+ emoji: null,
1627
+ text
1628
+ };
1615
1629
  }
1616
1630
  /**
1617
- * My Scheduled Messages
1631
+ * Formats a message channel name by replacing spaces and underscores with hyphens,
1632
+ * converting to lowercase, and removing any non-alphanumeric characters except hyphens.
1633
+ * @param name - The original channel name
1634
+ * @return The formatted channel name
1618
1635
  */
1619
- async function listMyScheduledMessages(config, input) {
1620
- return fetchMessaging(config, {
1621
- endpoint: `/v1/messaging/my_scheduled_messages`,
1622
- method: "GET",
1623
- input,
1624
- outputSchema: DraftMessageListResponseSchema
1625
- });
1636
+ function formatMessageChannelName$1(name) {
1637
+ return name.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "");
1638
+ }
1639
+ const MAX_GROUP_NAME_LENGTH$1 = 30;
1640
+ function formatGroupDisplayName$1(names, maxLength = MAX_GROUP_NAME_LENGTH$1) {
1641
+ const fullDisplayName = names.join(", ");
1642
+ if (fullDisplayName.length <= maxLength) return { displayName: fullDisplayName };
1643
+ for (let i = 0; i < names.length; i++) {
1644
+ const currentNames = names.slice(0, i + 1);
1645
+ const remainingCount = names.length - currentNames.length;
1646
+ if (remainingCount > 0) {
1647
+ const suffix = `+ ${remainingCount} other${remainingCount > 1 ? "s" : ""}`;
1648
+ if (`${currentNames.join(", ")} ${suffix}`.length > maxLength && i > 0) {
1649
+ const namesToShow = names.slice(0, i);
1650
+ const othersCount = names.length - i;
1651
+ const othersSuffix = `+${othersCount} other${othersCount > 1 ? "s" : ""}`;
1652
+ const truncatedDisplayName = namesToShow.join(", ");
1653
+ if (othersCount === 1) {
1654
+ const nameWithOneMore = names.slice(0, i + 1).join(", ");
1655
+ if (nameWithOneMore.length < truncatedDisplayName.length) return { displayName: nameWithOneMore };
1656
+ }
1657
+ return {
1658
+ displayName: truncatedDisplayName,
1659
+ remainder: othersSuffix
1660
+ };
1661
+ }
1662
+ }
1663
+ }
1664
+ const firstName = names[0] || "";
1665
+ const othersCount = names.length - 1;
1666
+ if (othersCount > 0) return {
1667
+ displayName: firstName,
1668
+ remainder: `+ ${othersCount} other${othersCount > 1 ? "s" : ""}`
1669
+ };
1670
+ return { displayName: firstName };
1671
+ }
1672
+ //#endregion
1673
+ //#region ../../messaging/core/src/utils/file-kinds.ts
1674
+ const ServerFileKindEnum$1 = zod.z.enum([
1675
+ "image",
1676
+ "video",
1677
+ "pdf",
1678
+ "sheets",
1679
+ "powerpoint",
1680
+ "docx"
1681
+ ]);
1682
+ const fallbackFileType$1 = "docx";
1683
+ //#endregion
1684
+ //#region ../../messaging/core/src/messaging-api.ts
1685
+ const MessagingApiContext = (0, react.createContext)(null);
1686
+ const MessagingApiProvider = MessagingApiContext.Provider;
1687
+ function useMessagingApi() {
1688
+ const api = (0, react.useContext)(MessagingApiContext);
1689
+ if (!api) throw new Error("useMessagingApi must be used within a MessagingApiProvider");
1690
+ return api;
1626
1691
  }
1627
- zod.z.strictObject({ bot: zod.z.strictObject({
1628
- title: zod.z.string(),
1629
- description: zod.z.string().nullable().optional(),
1630
- external_uri: zod.z.string().url().nullable().optional(),
1631
- image_url: zod.z.string().url().nullable().optional(),
1632
- type: zod.z.string().optional(),
1633
- avatar_background_color: zod.z.string().nullable().optional(),
1634
- avatar_emoji: zod.z.string().nullable().optional(),
1635
- webhook_external_uri: zod.z.string().url().nullable().optional(),
1636
- webhook_auth_token: zod.z.string().nullable().optional(),
1637
- webhook_http_method: zod.z.string().nullable().optional(),
1638
- conversation_ids: zod.z.array(zod.z.number()).optional()
1639
- }) });
1640
1692
  //#endregion
1641
1693
  //#region ../../messaging/core/src/hooks/useConversations.ts
1642
- function useConversations(config, input) {
1694
+ function useConversations(input) {
1695
+ const api = useMessagingApi();
1643
1696
  return (0, _tanstack_react_query.useInfiniteQuery)({
1644
1697
  queryKey: [...MESSAGES_QUERY_KEYS.listConversations(), input ?? {}],
1645
- queryFn: ({ pageParam }) => listConversations(config, {
1698
+ queryFn: ({ pageParam }) => api.listConversations({
1646
1699
  ...input || {},
1647
1700
  page: pageParam ?? 1
1648
1701
  }),
@@ -1652,14 +1705,15 @@ function useConversations(config, input) {
1652
1705
  ...WEBSOCKET_QUERY_OPTIONS
1653
1706
  });
1654
1707
  }
1655
- function useConversation(config, conversationId) {
1708
+ function useConversation(conversationId) {
1709
+ const api = useMessagingApi();
1656
1710
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
1657
1711
  return (0, _tanstack_react_query.useQuery)({
1658
1712
  queryKey: MESSAGES_QUERY_KEYS.conversationMetadata(conversationId),
1659
1713
  queryFn: async () => {
1660
1714
  const fromSeed = seedConversationMetadataFromLists(queryClient, conversationId);
1661
1715
  if (fromSeed) return fromSeed;
1662
- const result = await getConversation(config, conversationId);
1716
+ const result = await api.getConversation(conversationId);
1663
1717
  applyConversationMetaToCaches({
1664
1718
  queryClient,
1665
1719
  conversation: {
@@ -1675,8 +1729,9 @@ function useConversation(config, conversationId) {
1675
1729
  }
1676
1730
  //#endregion
1677
1731
  //#region ../../messaging/core/src/hooks/useFindConversation.ts
1678
- function useFindConversation(config, params) {
1732
+ function useFindConversation(params) {
1679
1733
  const { recipients, contact_id, conversation_type, enabled } = params;
1734
+ const api = useMessagingApi();
1680
1735
  const useRecipientsFlow = Array.isArray(recipients) && recipients.length > 0;
1681
1736
  const inferredType = conversation_type || (useRecipientsFlow ? recipients.length > 1 ? "GroupConversation" : "DirectConversation" : void 0);
1682
1737
  return (0, _tanstack_react_query.useQuery)({
@@ -1690,11 +1745,11 @@ function useFindConversation(config, params) {
1690
1745
  conversation_type
1691
1746
  ],
1692
1747
  queryFn: async () => {
1693
- if (useRecipientsFlow) return await createConversation(config, {
1748
+ if (useRecipientsFlow) return await api.createConversation({
1694
1749
  conversation: { type: inferredType },
1695
1750
  recipients: recipients.map((r) => ({ uid: r.uid }))
1696
1751
  });
1697
- if (typeof contact_id === "number" && conversation_type) return await findConversation(config, {
1752
+ if (typeof contact_id === "number" && conversation_type) return await api.findConversation({
1698
1753
  contact_id,
1699
1754
  conversation_type
1700
1755
  });
@@ -1706,20 +1761,22 @@ function useFindConversation(config, params) {
1706
1761
  }
1707
1762
  //#endregion
1708
1763
  //#region ../../messaging/core/src/hooks/useAnnouncementChannel.ts
1709
- function useAnnouncementChannel(config) {
1764
+ function useAnnouncementChannel() {
1765
+ const api = useMessagingApi();
1710
1766
  return (0, _tanstack_react_query.useQuery)({
1711
1767
  queryKey: MESSAGES_QUERY_KEYS.announcementChannel(),
1712
- queryFn: () => getAnnouncementChannel(config),
1768
+ queryFn: () => api.getAnnouncementChannel(),
1713
1769
  staleTime: Infinity,
1714
1770
  refetchOnWindowFocus: false
1715
1771
  });
1716
1772
  }
1717
1773
  //#endregion
1718
1774
  //#region ../../messaging/core/src/hooks/useScheduledMessages.ts
1719
- function useScheduledMessages$1(config, conversationId, perPage = 20) {
1775
+ function useScheduledMessages$1(conversationId, perPage = 20) {
1776
+ const api = useMessagingApi();
1720
1777
  return (0, _tanstack_react_query.useInfiniteQuery)({
1721
1778
  queryKey: MESSAGES_QUERY_KEYS.listScheduledMessages(conversationId),
1722
- queryFn: async ({ pageParam }) => listScheduledMessages(config, conversationId, {
1779
+ queryFn: async ({ pageParam }) => api.listScheduledMessages(conversationId, {
1723
1780
  page: pageParam ?? 1,
1724
1781
  per_page: perPage
1725
1782
  }),
@@ -1734,10 +1791,11 @@ function useScheduledMessages$1(config, conversationId, perPage = 20) {
1734
1791
  }
1735
1792
  //#endregion
1736
1793
  //#region ../../messaging/core/src/hooks/useMyScheduledMessages.ts
1737
- function useMyScheduledMessages(config) {
1794
+ function useMyScheduledMessages() {
1795
+ const api = useMessagingApi();
1738
1796
  return (0, _tanstack_react_query.useInfiniteQuery)({
1739
1797
  queryKey: MESSAGES_QUERY_KEYS.listMyScheduledMessages(),
1740
- queryFn: async ({ pageParam }) => listMyScheduledMessages(config, {
1798
+ queryFn: async ({ pageParam }) => api.listMyScheduledMessages({
1741
1799
  page: pageParam ?? 1,
1742
1800
  per_page: 50
1743
1801
  }),
@@ -1749,12 +1807,13 @@ function useMyScheduledMessages(config) {
1749
1807
  }
1750
1808
  //#endregion
1751
1809
  //#region ../../messaging/core/src/hooks/useReactions.ts
1752
- function useReaction(config) {
1810
+ function useReaction() {
1811
+ const api = useMessagingApi();
1753
1812
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
1754
1813
  const reactMutation = (0, _tanstack_react_query.useMutation)({
1755
1814
  mutationFn: async (params) => {
1756
1815
  const { conversationId, messageId, reactionString } = params;
1757
- return reactToMessage(config, conversationId, {
1816
+ return api.reactToMessage(conversationId, {
1758
1817
  message_id: messageId,
1759
1818
  reaction: reactionString
1760
1819
  });
@@ -1802,7 +1861,7 @@ function useReaction(config) {
1802
1861
  const unreactMutation = (0, _tanstack_react_query.useMutation)({
1803
1862
  mutationFn: async (params) => {
1804
1863
  const { conversationId, messageId } = params;
1805
- return unreactToMessage(config, conversationId, { message_id: messageId });
1864
+ return api.unreactToMessage(conversationId, { message_id: messageId });
1806
1865
  },
1807
1866
  onMutate: async (variables) => {
1808
1867
  const { conversationId, messageId } = variables;
@@ -1851,8 +1910,9 @@ function useReaction(config) {
1851
1910
  }
1852
1911
  //#endregion
1853
1912
  //#region ../../messaging/core/src/hooks/useMessages.ts
1854
- function useMessages$1(config, params, options) {
1913
+ function useMessages$1(params, options) {
1855
1914
  const { conversationId, messageId, perPage = 50 } = params;
1915
+ const api = useMessagingApi();
1856
1916
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
1857
1917
  const isBottomAnchored = !messageId;
1858
1918
  const query = (0, _tanstack_react_query.useInfiniteQuery)({
@@ -1861,7 +1921,7 @@ function useMessages$1(config, params, options) {
1861
1921
  const param = pageParam;
1862
1922
  const pageNumber = param?.type === "page" ? param.number ?? 1 : void 0;
1863
1923
  const shouldMarkRead = isBottomAnchored && pageNumber === 1;
1864
- const result = await listMessages(config, conversationId, param?.type === "message" ? {
1924
+ const input = param?.type === "message" ? {
1865
1925
  preview: true,
1866
1926
  message_id: param.id,
1867
1927
  per_page: perPage
@@ -1869,7 +1929,8 @@ function useMessages$1(config, params, options) {
1869
1929
  preview: !shouldMarkRead ? true : false,
1870
1930
  page: pageNumber ?? 1,
1871
1931
  per_page: perPage
1872
- });
1932
+ };
1933
+ const result = await api.listMessages(conversationId, input);
1873
1934
  if (shouldMarkRead) {
1874
1935
  applyConversationMetaToCaches({
1875
1936
  queryClient,
@@ -1938,7 +1999,8 @@ function useMessages$1(config, params, options) {
1938
1999
  }
1939
2000
  //#endregion
1940
2001
  //#region ../../messaging/core/src/hooks/useSendMessage.ts
1941
- function useSendMessage$1(config, conversationId, user, options) {
2002
+ function useSendMessage$1(conversationId, user, options) {
2003
+ const api = useMessagingApi();
1942
2004
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
1943
2005
  const clientMessageIdRef = (0, react.useRef)(null);
1944
2006
  return (0, _tanstack_react_query.useMutation)({
@@ -1961,8 +2023,8 @@ function useSendMessage$1(config, conversationId, user, options) {
1961
2023
  },
1962
2024
  client_message_id: clientMessageIdRef.current ?? (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`)
1963
2025
  };
1964
- if (input.replyToId) return replyToMessage(config, conversationId, input.replyToId, payload);
1965
- return createMessage(config, conversationId, payload);
2026
+ if (input.replyToId) return api.replyToMessage(conversationId, input.replyToId, payload);
2027
+ return api.createMessage(conversationId, payload);
1966
2028
  },
1967
2029
  onMutate: async (input) => {
1968
2030
  const rnd = Math.floor(Math.random() * 2147483647);
@@ -2072,8 +2134,9 @@ function useSendMessage$1(config, conversationId, user, options) {
2072
2134
  }
2073
2135
  //#endregion
2074
2136
  //#region ../../messaging/core/src/hooks/useUpdateScheduledMessage.ts
2075
- function useUpdateScheduledMessage$1(config, params, callbacks) {
2137
+ function useUpdateScheduledMessage$1(params, callbacks) {
2076
2138
  const { conversationId, editScheduledId, editScheduledMessage, conversationType } = params;
2139
+ const api = useMessagingApi();
2077
2140
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
2078
2141
  return (0, _tanstack_react_query.useMutation)({
2079
2142
  mutationFn: async ({ body, attachments }) => {
@@ -2091,14 +2154,14 @@ function useUpdateScheduledMessage$1(config, params, callbacks) {
2091
2154
  const scheduledAtISO = editScheduledMessage?.scheduled_at || editScheduledMessage?.metadata?.scheduled_at || null;
2092
2155
  if (!scheduledAtISO) throw new Error("Missing scheduled time for the message");
2093
2156
  const messageType = mapConversationTypeToMessageType(conversationType);
2094
- await createScheduledMessage(config, conversationId, { scheduled_message: {
2157
+ await api.createScheduledMessage(conversationId, { scheduled_message: {
2095
2158
  body,
2096
2159
  theme: "inquiry",
2097
2160
  scheduled_at: new Date(scheduledAtISO).toISOString(),
2098
2161
  type: messageType,
2099
2162
  attachments_attributes: mappedAttachments
2100
2163
  } });
2101
- await destroyScheduledMessage(config, conversationId, editScheduledId);
2164
+ await api.destroyScheduledMessage(conversationId, editScheduledId);
2102
2165
  },
2103
2166
  onSuccess: async () => {
2104
2167
  await Promise.all([queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listScheduledMessages(conversationId) }), queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listMyScheduledMessages() })]);
@@ -2580,6 +2643,39 @@ var require_actioncable = /* @__PURE__ */ require_chunk.__commonJSMin(((exports,
2580
2643
  }));
2581
2644
  //#endregion
2582
2645
  //#region ../../messaging/core/src/cable/schemas.ts
2646
+ /**
2647
+ * Locally-defined schemas for WebSocket event parsing.
2648
+ * These replace imports from the API client to keep core dependency-free.
2649
+ */
2650
+ const ReactionStatsSchema = zod.z.record(zod.z.string(), zod.z.number());
2651
+ /**
2652
+ * Lenient schema for parsing incoming WebSocket message payloads.
2653
+ * Uses passthrough() to accept extra/unknown fields from the server.
2654
+ */
2655
+ const MessageItemSchema = zod.z.object({
2656
+ id: zod.z.number(),
2657
+ body: zod.z.string(),
2658
+ type: zod.z.string(),
2659
+ created_at: zod.z.string(),
2660
+ updated_at: zod.z.string().nullable().optional(),
2661
+ deleted_at: zod.z.string().nullable().optional(),
2662
+ edited_at: zod.z.string().nullable().optional(),
2663
+ conversation_id: zod.z.number(),
2664
+ status: zod.z.string(),
2665
+ attachments: zod.z.array(zod.z.any()),
2666
+ reaction_stats: zod.z.record(zod.z.string(), zod.z.number()).nullable(),
2667
+ reaction: zod.z.string().nullable().optional(),
2668
+ metadata: zod.z.any().optional(),
2669
+ emoji_only_count: zod.z.number().optional(),
2670
+ mentioned_recipients: zod.z.array(zod.z.any()),
2671
+ sent_at: zod.z.string().optional(),
2672
+ pinned: zod.z.boolean().optional(),
2673
+ scheduled_at: zod.z.string().optional(),
2674
+ replied_to_message: zod.z.any().nullable().optional(),
2675
+ sender_recipient: zod.z.any(),
2676
+ seen_by: zod.z.unknown(),
2677
+ seen_by_count: zod.z.unknown()
2678
+ }).passthrough();
2583
2679
  const NewMessageSchema = zod.z.strictObject({
2584
2680
  type: zod.z.literal("new_message"),
2585
2681
  message: zod.z.string(),
@@ -3134,7 +3230,7 @@ function createFluidFileUploader(apiKey) {
3134
3230
  //#endregion
3135
3231
  //#region ../../messaging/ui/src/app/MessagingAppProvider.tsx
3136
3232
  const MessagingAppContext = (0, react.createContext)(null);
3137
- function MessagingAppProvider({ config, auth, websocketUrl, token, renderImage, renderProfileTrigger, onNavigate, initialRoute, children }) {
3233
+ function MessagingAppProvider({ api, auth, websocketUrl, token, renderImage, renderProfileTrigger, onNavigate, initialRoute, children }) {
3138
3234
  const [currentRoute, setCurrentRoute] = (0, react.useState)(() => initialRoute ?? { view: "index" });
3139
3235
  const navigation = {
3140
3236
  navigate: (0, react.useCallback)((route) => {
@@ -3150,17 +3246,19 @@ function MessagingAppProvider({ config, auth, websocketUrl, token, renderImage,
3150
3246
  }, [currentRoute.view, onNavigate]),
3151
3247
  currentRoute
3152
3248
  };
3153
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagingAppContext.Provider, {
3154
- value: {
3155
- config,
3156
- auth,
3157
- websocketUrl,
3158
- token,
3159
- renderImage,
3160
- renderProfileTrigger,
3161
- navigation
3162
- },
3163
- children
3249
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagingApiProvider, {
3250
+ value: api,
3251
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagingAppContext.Provider, {
3252
+ value: {
3253
+ auth,
3254
+ websocketUrl,
3255
+ token,
3256
+ renderImage,
3257
+ renderProfileTrigger,
3258
+ navigation
3259
+ },
3260
+ children
3261
+ })
3164
3262
  });
3165
3263
  }
3166
3264
  function useMessagingApp() {
@@ -3390,12 +3488,12 @@ function getPageTitleFromRoute(route) {
3390
3488
  }
3391
3489
  }
3392
3490
  function MessagesTopBar({ renderSidebarTrigger, renderBreadcrumb, history, onToast }) {
3393
- const { config, auth, renderImage: contextRenderImage } = useMessagingApp();
3491
+ const { auth, renderImage: contextRenderImage } = useMessagingApp();
3394
3492
  const navigation = useMessagingNavigation();
3395
3493
  const currentRoute = navigation.currentRoute;
3396
3494
  const renderImage = contextRenderImage ?? defaultRenderImage;
3397
3495
  const conversationId = "conversationId" in currentRoute ? currentRoute.conversationId : 0;
3398
- const { data: conversation } = useConversation(config, conversationId);
3496
+ const { data: conversation } = useConversation(conversationId);
3399
3497
  const currentUser = auth.currentUser;
3400
3498
  const affiliateId = currentUser?.affiliateId;
3401
3499
  const otherUsers = conversation?.recipients.filter((r) => r.receivable_id !== affiliateId);
@@ -3855,9 +3953,9 @@ function Channel({ item, renderImage }) {
3855
3953
  })] });
3856
3954
  }
3857
3955
  function SidebarItem({ item }) {
3858
- const { config, auth, renderImage: contextRenderImage } = useMessagingApp();
3956
+ const { auth, renderImage: contextRenderImage } = useMessagingApp();
3859
3957
  const navigation = useMessagingNavigation();
3860
- const { data: meta } = useConversation(config, item.id);
3958
+ const { data: meta } = useConversation(item.id);
3861
3959
  const renderImage = contextRenderImage ?? defaultRenderImage;
3862
3960
  const currentUser = auth.currentUser;
3863
3961
  if (!currentUser) return null;
@@ -4021,7 +4119,6 @@ function useScrollerRef() {
4021
4119
  return context;
4022
4120
  }
4023
4121
  function MessagingLayout({ children, topBar, renderLockedDownlineItem, renderNewChannelSidebar, showAdminFeatures = true, canUseMyDownline = false, downlineData }) {
4024
- const { config } = useMessagingApp();
4025
4122
  const navigation = useMessagingNavigation();
4026
4123
  const [isDragging, setIsDragging] = (0, react.useState)(false);
4027
4124
  const sidebarRef = (0, react.useRef)(null);
@@ -4059,8 +4156,8 @@ function MessagingLayout({ children, topBar, renderLockedDownlineItem, renderNew
4059
4156
  handleMouseMove,
4060
4157
  handleMouseUp
4061
4158
  ]);
4062
- const result = useConversations(config, { per_page: CHANNELS_PER_FETCH });
4063
- const { data: announcementChannel, isLoading: isLoadingAnnouncementChannel } = useAnnouncementChannel(config);
4159
+ const result = useConversations({ per_page: CHANNELS_PER_FETCH });
4160
+ const { data: announcementChannel, isLoading: isLoadingAnnouncementChannel } = useAnnouncementChannel();
4064
4161
  const hasMyDownline = downlineData?.hasDownline ?? false;
4065
4162
  const myDownlineConversation = downlineData?.conversation ?? null;
4066
4163
  const allEntries = result.data?.pages.flatMap((page) => page[1].items) ?? [];
@@ -42958,11 +43055,12 @@ function Timestamp({ children: timestamp, className = "", displayMode = "auto" }
42958
43055
  }
42959
43056
  //#endregion
42960
43057
  //#region ../../messaging/ui/src/components/Message.tsx
42961
- function Message({ conversation, message, isGrouped, highlightType = "auto", config, renderProfileTrigger, renderProfileContent, renderImage, onPinError, onCopyLinkSuccess, onCopyLinkError, getMessageLink }) {
43058
+ function Message({ conversation, message, isGrouped, highlightType = "auto", renderProfileTrigger, renderProfileContent, renderImage, onPinError, onCopyLinkSuccess, onCopyLinkError, getMessageLink }) {
42962
43059
  const [emojiPickerOpen, setEmojiPickerOpen] = (0, react.useState)(false);
42963
43060
  const [optionsOpen, setOptionsOpen] = (0, react.useState)(false);
42964
43061
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
42965
- const { react: react$1, unreact } = useReaction(config);
43062
+ const { react: react$1, unreact } = useReaction();
43063
+ const api = useMessagingApi();
42966
43064
  const addReaction = (reaction) => {
42967
43065
  react$1({
42968
43066
  conversationId: conversation.id,
@@ -42988,7 +43086,8 @@ function Message({ conversation, message, isGrouped, highlightType = "auto", con
42988
43086
  });
42989
43087
  setOptimisticPinned(newPinned);
42990
43088
  try {
42991
- await (newPinned ? pinMessage : unpinMessage)(config, conversation.id, message.id);
43089
+ if (newPinned) await api.pinMessage(conversation.id, message.id);
43090
+ else await api.unpinMessage(conversation.id, message.id);
42992
43091
  } catch {
42993
43092
  applyMessagePinnedFlag({
42994
43093
  queryClient,
@@ -43652,8 +43751,8 @@ const deepEqual = (obj1, obj2) => {
43652
43751
  * Hook to manage conversation draft state using React Query as the single source of truth.
43653
43752
  * Initializes with data from conversation metadata to avoid loading delays.
43654
43753
  */
43655
- function useConversationDraft(config, conversationId) {
43656
- const { data: conversationMetadata } = useConversation(config, conversationId);
43754
+ function useConversationDraft(conversationId) {
43755
+ const { data: conversationMetadata } = useConversation(conversationId);
43657
43756
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
43658
43757
  const isLocalDraft = !conversationId;
43659
43758
  const abortFunctionsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
@@ -43978,13 +44077,14 @@ function useIsDragging() {
43978
44077
  }
43979
44078
  //#endregion
43980
44079
  //#region ../../messaging/ui/src/hooks/use-save-draft.ts
43981
- function useSaveDraft(config, conversationId, options) {
44080
+ function useSaveDraft(conversationId, options) {
43982
44081
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
44082
+ const api = useMessagingApi();
43983
44083
  return (0, _tanstack_react_query.useMutation)({
43984
44084
  mutationFn: async () => {
43985
44085
  if (!conversationId) return null;
43986
44086
  try {
43987
- await destroyDraft(config, conversationId);
44087
+ await api.destroyDraft(conversationId);
43988
44088
  } catch (err) {
43989
44089
  if (err.status !== 404) console.warn("destroyDraft failed (continuing with save):", err);
43990
44090
  }
@@ -44002,7 +44102,7 @@ function useSaveDraft(config, conversationId, options) {
44002
44102
  const bodyIsEmpty = !latestBody.trim();
44003
44103
  const hasFiles = latestAttachments.length > 0;
44004
44104
  if (bodyIsEmpty && !hasFiles) return {};
44005
- return await saveDraft(config, conversationId, { draft_message: {
44105
+ return await api.saveDraft(conversationId, { draft_message: {
44006
44106
  body: latestBody,
44007
44107
  attachments_attributes: hasFiles ? latestAttachments : void 0
44008
44108
  } });
@@ -44038,10 +44138,10 @@ function useSaveDraft(config, conversationId, options) {
44038
44138
  }
44039
44139
  });
44040
44140
  }
44041
- function useDebouncedSaveDraft(config, conversationId, options) {
44141
+ function useDebouncedSaveDraft(conversationId, options) {
44042
44142
  const debounceMs = options?.debounceMs ?? 1e3;
44043
44143
  const debounceTimerRef = (0, react.useRef)(null);
44044
- const saveDraftMutation = useSaveDraft(config, conversationId, { onError: options?.onError });
44144
+ const saveDraftMutation = useSaveDraft(conversationId, { onError: options?.onError });
44045
44145
  const debouncedSave = () => {
44046
44146
  if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
44047
44147
  debounceTimerRef.current = setTimeout(() => {
@@ -57208,7 +57308,8 @@ function convertMentionTokensInJson(node, users) {
57208
57308
  }
57209
57309
  //#endregion
57210
57310
  //#region ../../messaging/ui/src/components/composer/useComposerActions.ts
57211
- function useComposerActions({ editor, onSend, onSaveScheduledEdit, isUploadingFiles, completedFiles, clearDebounce, saveImmediately, draft, mode, sendWithEnter, conversationType, conversationId, config, onScheduleSuccess, onScheduleComplete, onScheduleError }) {
57311
+ function useComposerActions({ editor, onSend, onSaveScheduledEdit, isUploadingFiles, completedFiles, clearDebounce, saveImmediately, draft, mode, sendWithEnter, conversationType, conversationId, onScheduleSuccess, onScheduleComplete, onScheduleError }) {
57312
+ const api = useMessagingApi();
57212
57313
  const handleSend = (0, react.useCallback)(async () => {
57213
57314
  if (!editor || !onSend) return;
57214
57315
  if (isUploadingFiles) return;
@@ -57278,7 +57379,7 @@ function useComposerActions({ editor, onSend, onSaveScheduledEdit, isUploadingFi
57278
57379
  return parsed.success ? parsed.data : fallbackFileType;
57279
57380
  };
57280
57381
  const messageType = mapConversationTypeToMessageType(conversationType);
57281
- await createScheduledMessage(config, conversationId, { scheduled_message: {
57382
+ await api.createScheduledMessage(conversationId, { scheduled_message: {
57282
57383
  body: markdown,
57283
57384
  theme: "inquiry",
57284
57385
  scheduled_at: scheduledAt.toISOString(),
@@ -57309,7 +57410,7 @@ function useComposerActions({ editor, onSend, onSaveScheduledEdit, isUploadingFi
57309
57410
  completedFiles,
57310
57411
  conversationId,
57311
57412
  conversationType,
57312
- config,
57413
+ api,
57313
57414
  draft,
57314
57415
  saveImmediately,
57315
57416
  onScheduleSuccess,
@@ -57321,11 +57422,11 @@ function useComposerActions({ editor, onSend, onSaveScheduledEdit, isUploadingFi
57321
57422
  }
57322
57423
  //#endregion
57323
57424
  //#region ../../messaging/ui/src/components/composer/MessageComposer.tsx
57324
- function MessageComposer({ onSend, onScheduleSuccess, onSaveScheduledEdit, placeholder = "Type your message...", disabled = false, mentionableUsers, conversationType, conversationId, saveDrafts = true, mode = "normal", initialEditData, config, uploader, canSchedule = false, renderUpgradePrompt, renderLinkModal, renderImage, onScheduleComplete, onScheduleError, onUploadError }) {
57425
+ function MessageComposer({ onSend, onScheduleSuccess, onSaveScheduledEdit, placeholder = "Type your message...", disabled = false, mentionableUsers, conversationType, conversationId, saveDrafts = true, mode = "normal", initialEditData, uploader, canSchedule = false, renderUpgradePrompt, renderLinkModal, renderImage, onScheduleComplete, onScheduleError, onUploadError }) {
57325
57426
  const [sendWithEnter] = use_local_storage_state_default("messages-send-with-enter", { defaultValue: true });
57326
57427
  const effectiveConversationId = saveDrafts ? conversationId : 0;
57327
- const draft = useConversationDraft(config, effectiveConversationId);
57328
- const { debouncedSave, saveImmediately, isSaving, clearDebounce } = useDebouncedSaveDraft(config, effectiveConversationId);
57428
+ const draft = useConversationDraft(effectiveConversationId);
57429
+ const { debouncedSave, saveImmediately, isSaving, clearDebounce } = useDebouncedSaveDraft(effectiveConversationId);
57329
57430
  const { handleFiles, handleRemoveFile } = useDraftFileUpload({
57330
57431
  uploader,
57331
57432
  draftApi: {
@@ -57449,7 +57550,6 @@ function MessageComposer({ onSend, onScheduleSuccess, onSaveScheduledEdit, place
57449
57550
  sendWithEnter,
57450
57551
  conversationType,
57451
57552
  conversationId,
57452
- config,
57453
57553
  onScheduleSuccess,
57454
57554
  onScheduleComplete,
57455
57555
  onScheduleError
@@ -57503,13 +57603,12 @@ function MessageComposer({ onSend, onScheduleSuccess, onSaveScheduledEdit, place
57503
57603
  }
57504
57604
  //#endregion
57505
57605
  //#region ../../messaging/ui/src/app/MessagesViewComposer.tsx
57506
- function MessagesViewComposer({ conversationId, conversationMetadata, config, uploader, saveDrafts, isEditingScheduled, editScheduledMessage, mentionableUsers, renderImage, isLoading, user, isSendPending, isUpdatePending, onSend, onSaveScheduledEdit, onScheduleSuccess }) {
57606
+ function MessagesViewComposer({ conversationId, conversationMetadata, uploader, saveDrafts, isEditingScheduled, editScheduledMessage, mentionableUsers, renderImage, isLoading, user, isSendPending, isUpdatePending, onSend, onSaveScheduledEdit, onScheduleSuccess }) {
57507
57607
  const input = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageComposer, {
57508
57608
  conversationId,
57509
57609
  conversationType: conversationMetadata?.type,
57510
57610
  saveDrafts: isEditingScheduled ? false : saveDrafts,
57511
57611
  mode: isEditingScheduled ? "editScheduled" : "normal",
57512
- config,
57513
57612
  uploader,
57514
57613
  initialEditData: isEditingScheduled && editScheduledMessage ? {
57515
57614
  body: editScheduledMessage.body || "",
@@ -57570,13 +57669,11 @@ function HeaderSlot({ render, conversation }) {
57570
57669
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: render(conversation) });
57571
57670
  }
57572
57671
  function ViewMessage({ conversation, message, isGrouped, highlightType = "auto", renderProfileTrigger, renderProfileContent, renderImage, onToast, getMessageLink }) {
57573
- const { config } = useMessagingApp();
57574
57672
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Message, {
57575
57673
  conversation,
57576
57674
  message,
57577
57675
  isGrouped,
57578
57676
  highlightType,
57579
- config,
57580
57677
  renderProfileTrigger,
57581
57678
  renderProfileContent,
57582
57679
  renderImage,
@@ -57618,7 +57715,7 @@ const getFallbackSystemMessageText = (senderName, conversationMetadata) => {
57618
57715
  };
57619
57716
  function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, renderProfileTrigger, renderProfileContent, onToast, getMessageLink, renderScheduledBanner, renderHeader, onMarkRead, uploader }) {
57620
57717
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
57621
- const { config, auth, renderImage: contextRenderImage } = useMessagingApp();
57718
+ const { auth, renderImage: contextRenderImage } = useMessagingApp();
57622
57719
  const navigation = useMessagingNavigation();
57623
57720
  const scrollerRef = useScrollerRef();
57624
57721
  const renderImage = contextRenderImage ?? defaultRenderImage;
@@ -57657,7 +57754,6 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
57657
57754
  scrollTo
57658
57755
  ]);
57659
57756
  const { messages, fetchNextPage, hasNextPage, isFetchingNextPage, fetchPreviousPage, hasPreviousPage, isFetchingPreviousPage, isLoading: isLoadingMessages, isError, error: messagesError } = useMessages({
57660
- config,
57661
57757
  conversationId,
57662
57758
  messageId: messageId ? Number(messageId) : void 0,
57663
57759
  perPage: 50,
@@ -57667,7 +57763,7 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
57667
57763
  const lastId = messages[messages.length - 1]?.id ?? "x";
57668
57764
  return `${messages.length}:${String(lastId)}`;
57669
57765
  }, [messages]);
57670
- const { data: conversationMetadata, error: metadataError, isLoading: isLoadingMetadata } = useConversation(config, conversationId);
57766
+ const { data: conversationMetadata, error: metadataError, isLoading: isLoadingMetadata } = useConversation(conversationId);
57671
57767
  (0, react.useEffect)(() => {
57672
57768
  baselineCapturedRef.current = false;
57673
57769
  setEntryUnreadBaselineId(null);
@@ -57686,7 +57782,7 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
57686
57782
  scrollToBottom,
57687
57783
  resetKey: conversationId
57688
57784
  });
57689
- const scheduledHook = useScheduledMessages(config, conversationId);
57785
+ const scheduledHook = useScheduledMessages(conversationId);
57690
57786
  const scheduledData = scheduledHook.data;
57691
57787
  const refetchScheduled = scheduledHook.refetch;
57692
57788
  const editScheduledId = (() => {
@@ -57725,7 +57821,6 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
57725
57821
  };
57726
57822
  }, [currentUser]);
57727
57823
  const sendMessageMutation = useSendMessage({
57728
- config,
57729
57824
  conversationId,
57730
57825
  user,
57731
57826
  onSuccess: () => {
@@ -57739,7 +57834,6 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
57739
57834
  }
57740
57835
  });
57741
57836
  const updateScheduledMutation = useUpdateScheduledMessage({
57742
- config,
57743
57837
  conversationId,
57744
57838
  editScheduledId,
57745
57839
  editScheduledMessage,
@@ -58037,7 +58131,6 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
58037
58131
  const composer = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagesViewComposer, {
58038
58132
  conversationId,
58039
58133
  conversationMetadata,
58040
- config,
58041
58134
  uploader,
58042
58135
  saveDrafts,
58043
58136
  isEditingScheduled,
@@ -58169,24 +58262,24 @@ function MessagesView({ conversationId, messageId, saveDrafts, noDateResults, re
58169
58262
  }, "composer")]
58170
58263
  }, "layer-1");
58171
58264
  }
58172
- function useMessages({ config, conversationId, messageId, perPage, onMarkRead }) {
58173
- return useMessages$1(config, {
58265
+ function useMessages({ conversationId, messageId, perPage, onMarkRead }) {
58266
+ return useMessages$1({
58174
58267
  conversationId,
58175
58268
  messageId,
58176
58269
  perPage
58177
58270
  }, { onMarkRead });
58178
58271
  }
58179
- function useSendMessage({ config, conversationId, user, onSuccess, onError }) {
58180
- return useSendMessage$1(config, conversationId, user, {
58272
+ function useSendMessage({ conversationId, user, onSuccess, onError }) {
58273
+ return useSendMessage$1(conversationId, user, {
58181
58274
  onSuccess,
58182
58275
  onError
58183
58276
  });
58184
58277
  }
58185
- function useScheduledMessages(config, conversationId) {
58186
- return useScheduledMessages$1(config, conversationId, 50);
58278
+ function useScheduledMessages(conversationId) {
58279
+ return useScheduledMessages$1(conversationId, 50);
58187
58280
  }
58188
- function useUpdateScheduledMessage({ config, conversationId, editScheduledId, editScheduledMessage, conversationType, onSuccess, onError }) {
58189
- return useUpdateScheduledMessage$1(config, {
58281
+ function useUpdateScheduledMessage({ conversationId, editScheduledId, editScheduledMessage, conversationType, onSuccess, onError }) {
58282
+ return useUpdateScheduledMessage$1({
58190
58283
  conversationId,
58191
58284
  editScheduledId,
58192
58285
  editScheduledMessage,
@@ -58199,13 +58292,12 @@ function useUpdateScheduledMessage({ config, conversationId, editScheduledId, ed
58199
58292
  //#endregion
58200
58293
  //#region ../../messaging/ui/src/app/PinnedMessagesView.tsx
58201
58294
  function PinnedMessage({ conversation, message, onToast, renderProfileContent, getMessageLink }) {
58202
- const { config, renderImage, renderProfileTrigger } = useMessagingApp();
58295
+ const { renderImage, renderProfileTrigger } = useMessagingApp();
58203
58296
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Message, {
58204
58297
  conversation,
58205
58298
  message,
58206
58299
  isGrouped: false,
58207
58300
  highlightType: "never",
58208
- config,
58209
58301
  renderProfileTrigger: renderProfileTrigger ? ({ children }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children }) : void 0,
58210
58302
  renderProfileContent,
58211
58303
  renderImage,
@@ -58216,12 +58308,12 @@ function PinnedMessage({ conversation, message, onToast, renderProfileContent, g
58216
58308
  });
58217
58309
  }
58218
58310
  function PinnedMessagesView({ conversationId, onToast, renderProfileContent, getMessageLink }) {
58219
- const { config } = useMessagingApp();
58220
58311
  const { navigate } = useMessagingNavigation();
58312
+ const api = useMessagingApi();
58221
58313
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error } = (0, _tanstack_react_query.useInfiniteQuery)({
58222
58314
  queryKey: MESSAGES_QUERY_KEYS.listPinnedMessages(conversationId),
58223
58315
  queryFn: async ({ pageParam }) => {
58224
- return listPinnedMessages(config, conversationId, {
58316
+ return api.listPinnedMessages(conversationId, {
58225
58317
  page: pageParam,
58226
58318
  per_page: 50
58227
58319
  });
@@ -58232,7 +58324,7 @@ function PinnedMessagesView({ conversationId, onToast, renderProfileContent, get
58232
58324
  staleTime: PINNED_MESSAGES_STALE_TIME,
58233
58325
  refetchInterval: PINNED_MESSAGES_REFETCH_INTERVAL
58234
58326
  });
58235
- const { data: conversationMetadata } = useConversation(config, conversationId);
58327
+ const { data: conversationMetadata } = useConversation(conversationId);
58236
58328
  const messages = (0, react.useMemo)(() => {
58237
58329
  if (!data?.pages) return [];
58238
58330
  return data.pages.flatMap((pageData) => pageData[1].items);
@@ -58849,10 +58941,9 @@ function getConversationIdFromRecipients(recipients) {
58849
58941
  return conversationRecipient ? conversationRecipient.id : null;
58850
58942
  }
58851
58943
  function NewMessageView({ searchUsers, searchChannels, getCachedChannels, messagesViewProps }) {
58852
- const { config } = useMessagingApp();
58853
58944
  const [selectedRecipients, setSelectedRecipients] = (0, react.useState)([]);
58854
58945
  const selectedConversationId = (0, react.useMemo)(() => getConversationIdFromRecipients(selectedRecipients), [selectedRecipients]);
58855
- const { data: foundConversation } = useFindConversation(config, {
58946
+ const { data: foundConversation } = useFindConversation({
58856
58947
  recipients: selectedRecipients.length > 0 && !selectedConversationId ? selectedRecipients.map((r) => ({ uid: r.id })) : void 0,
58857
58948
  enabled: selectedRecipients.length > 0 && !selectedConversationId
58858
58949
  });
@@ -59112,9 +59203,10 @@ function formatScheduledShort(iso) {
59112
59203
  }
59113
59204
  }
59114
59205
  function ScheduledMessageRow({ message, onToast }) {
59115
- const { config, auth, renderImage } = useMessagingApp();
59206
+ const { auth, renderImage } = useMessagingApp();
59116
59207
  const { navigate } = useMessagingNavigation();
59117
59208
  const queryClient = (0, _tanstack_react_query.useQueryClient)();
59209
+ const api = useMessagingApi();
59118
59210
  const [openConfirm, setOpenConfirm] = (0, react.useState)(false);
59119
59211
  const [openDeleteConfirm, setOpenDeleteConfirm] = (0, react.useState)(false);
59120
59212
  const [openReschedule, setOpenReschedule] = (0, react.useState)(false);
@@ -59123,7 +59215,7 @@ function ScheduledMessageRow({ message, onToast }) {
59123
59215
  const conversation = message.conversation;
59124
59216
  const rawConversationId = conversation?.id ?? message.conversation_id ?? 0;
59125
59217
  const conversationId = typeof rawConversationId === "string" ? Number.parseInt(rawConversationId, 10) : Number(rawConversationId);
59126
- const { data: fullConversation } = useConversation(config, conversationId);
59218
+ const { data: fullConversation } = useConversation(conversationId);
59127
59219
  const messageType = message.type;
59128
59220
  const effectiveConversation = fullConversation || conversation;
59129
59221
  const affiliateId = auth.currentUser?.affiliateId;
@@ -59161,12 +59253,13 @@ function ScheduledMessageRow({ message, onToast }) {
59161
59253
  metadata: a.metadata ?? void 0
59162
59254
  };
59163
59255
  });
59164
- await createMessage(config, conversationId, { message: {
59165
- type: mapConversationTypeToMessageType(fullConversation.type),
59256
+ const typeFromConversation = mapConversationTypeToMessageType(fullConversation.type);
59257
+ await api.createMessage(conversationId, { message: {
59258
+ type: typeFromConversation,
59166
59259
  body: message.body || "",
59167
59260
  attachments_attributes: mappedAttachments
59168
59261
  } });
59169
- await destroyScheduledMessage(config, conversationId, message.id);
59262
+ await api.destroyScheduledMessage(conversationId, message.id);
59170
59263
  },
59171
59264
  onSuccess: async () => {
59172
59265
  await Promise.all([
@@ -59183,7 +59276,7 @@ function ScheduledMessageRow({ message, onToast }) {
59183
59276
  });
59184
59277
  const deleteMutation = (0, _tanstack_react_query.useMutation)({
59185
59278
  mutationFn: async () => {
59186
- await destroyScheduledMessage(config, conversationId, message.id);
59279
+ await api.destroyScheduledMessage(conversationId, message.id);
59187
59280
  },
59188
59281
  onSuccess: async () => {
59189
59282
  await Promise.all([queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listScheduledMessages(conversationId) }), queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listMyScheduledMessages() })]);
@@ -59193,7 +59286,7 @@ function ScheduledMessageRow({ message, onToast }) {
59193
59286
  });
59194
59287
  const rescheduleMutation = (0, _tanstack_react_query.useMutation)({
59195
59288
  mutationFn: async (isoDate) => {
59196
- await updateScheduledMessage(config, conversationId, message.id, { scheduled_message: { scheduled_at: isoDate } });
59289
+ await api.updateScheduledMessage(conversationId, message.id, { scheduled_message: { scheduled_at: isoDate } });
59197
59290
  },
59198
59291
  onSuccess: async () => {
59199
59292
  await Promise.all([queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listScheduledMessages(conversationId) }), queryClient.invalidateQueries({ queryKey: MESSAGES_QUERY_KEYS.listMyScheduledMessages() })]);
@@ -59343,8 +59436,7 @@ function ScheduledMessageRow({ message, onToast }) {
59343
59436
  //#endregion
59344
59437
  //#region ../../messaging/ui/src/app/ScheduledMessagesView.tsx
59345
59438
  function ScheduledMessagesView({ onToast }) {
59346
- const { config } = useMessagingApp();
59347
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error } = useMyScheduledMessages(config);
59439
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, error } = useMyScheduledMessages();
59348
59440
  const messages = (0, react.useMemo)(() => {
59349
59441
  if (!data?.pages) return [];
59350
59442
  return data.pages.flatMap((p) => p[1].items);
@@ -59409,9 +59501,9 @@ function ScheduledMessagesView({ onToast }) {
59409
59501
  }
59410
59502
  //#endregion
59411
59503
  //#region ../../messaging/ui/src/app/MessagingApp.tsx
59412
- function MessagingApp({ config, auth, websocketUrl, token, renderImage, renderProfileTrigger, onNavigate, initialRoute, renderLockedDownlineItem, renderNewChannelSidebar, showAdminFeatures, canUseMyDownline, downlineData, topBar, messagesViewProps, newMessageViewProps, onToast, getMessageLink, renderProfileContent, children }) {
59504
+ function MessagingApp({ api, auth, websocketUrl, token, renderImage, renderProfileTrigger, onNavigate, initialRoute, renderLockedDownlineItem, renderNewChannelSidebar, showAdminFeatures, canUseMyDownline, downlineData, topBar, messagesViewProps, newMessageViewProps, onToast, getMessageLink, renderProfileContent, children }) {
59413
59505
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(MessagingAppProvider, {
59414
- config,
59506
+ api,
59415
59507
  auth,
59416
59508
  websocketUrl,
59417
59509
  token,
@@ -59497,14 +59589,14 @@ function defaultToast(message, type) {
59497
59589
  }
59498
59590
  function MessagingScreen({ onToast, filestackApiKey, websocketUrl: websocketUrlOverride, background, textColor, accentColor, padding, borderRadius, ...divProps }) {
59499
59591
  const { config } = require_FluidProvider.useFluidContext();
59500
- const { apiConfig, websocketUrl, token } = useMessagingConfig();
59592
+ const { apiConfig, messagingApi, websocketUrl, token } = useMessagingConfig();
59501
59593
  const messagingAuth = useMessagingAuth();
59502
59594
  const effectiveApiKey = filestackApiKey ?? config.filestackApiKey;
59503
59595
  const uploader = (0, react.useMemo)(() => createFluidFileUploader(effectiveApiKey), [effectiveApiKey]);
59504
59596
  const effectiveWsUrl = websocketUrlOverride ?? websocketUrl;
59505
59597
  const effectiveToast = onToast ?? defaultToast;
59506
59598
  const searchUsers = (0, react.useCallback)(async (query) => {
59507
- return ((await listConnectedRecipients(apiConfig, {
59599
+ return ((await messagingApi.listConnectedRecipients({
59508
59600
  filterrific: { search_query: query },
59509
59601
  per_page: 10,
59510
59602
  page: 1
@@ -59524,7 +59616,7 @@ function MessagingScreen({ onToast, filestackApiKey, websocketUrl: websocketUrlO
59524
59616
  conversationName: name
59525
59617
  };
59526
59618
  });
59527
- }, [apiConfig]);
59619
+ }, [messagingApi]);
59528
59620
  const searchChannels = (0, react.useCallback)(async (query) => {
59529
59621
  return (await searchConversations(apiConfig, { filterrific: { search_query: query } }) ?? []).map((channel) => {
59530
59622
  const { text: nameWithoutEmoji } = extractEmoji(channel.name);
@@ -59567,7 +59659,7 @@ function MessagingScreen({ onToast, filestackApiKey, websocketUrl: websocketUrlO
59567
59659
  ...divProps,
59568
59660
  className: `h-full ${divProps.className ?? ""}`,
59569
59661
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagingApp, {
59570
- config: apiConfig,
59662
+ api: messagingApi,
59571
59663
  auth: messagingAuth,
59572
59664
  websocketUrl: effectiveWsUrl,
59573
59665
  token,
@@ -59627,4 +59719,4 @@ Object.defineProperty(exports, "useMessagingConfig", {
59627
59719
  }
59628
59720
  });
59629
59721
 
59630
- //# sourceMappingURL=MessagingScreen-B9CCsimy.cjs.map
59722
+ //# sourceMappingURL=MessagingScreen-Cgx3jwpr.cjs.map