@adatechnology/conversations-ui 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4040,7 +4040,30 @@ function MessagesWorkspace({
4040
4040
  }
4041
4041
 
4042
4042
  // src/hooks/useConversationMessages.ts
4043
- import { useCallback as useCallback7 } from "react";
4043
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo3, useRef as useRef7, useState as useState15 } from "react";
4044
+ var OLDER_MESSAGES_PAGE_LIMIT = 50;
4045
+ function dedupeMessagesById(messages) {
4046
+ const seenIds = /* @__PURE__ */ new Set();
4047
+ const deduped = [];
4048
+ for (const message of messages) {
4049
+ if (seenIds.has(message.id)) continue;
4050
+ seenIds.add(message.id);
4051
+ deduped.push(message);
4052
+ }
4053
+ return deduped;
4054
+ }
4055
+ function prependOlderMessages(accumulated, olderPage) {
4056
+ const existingIds = new Set(accumulated.map((message) => message.id));
4057
+ const newMessages = olderPage.filter((message) => !existingIds.has(message.id));
4058
+ return [...newMessages, ...accumulated];
4059
+ }
4060
+ function shouldFetchOlderMessages(state) {
4061
+ if (state.isLoading || !state.hasMore) return false;
4062
+ return Boolean(state.oldestMessage);
4063
+ }
4064
+ function pageIndicatesMoreOlderMessages(pageLength, limit) {
4065
+ return pageLength >= limit;
4066
+ }
4044
4067
  function useConversationMessages(conversationId, params) {
4045
4068
  const context = useConversations();
4046
4069
  if (!context) {
@@ -4051,6 +4074,38 @@ function useConversationMessages(conversationId, params) {
4051
4074
  () => api.fetchMessages(conversationId, params),
4052
4075
  [conversationId, params?.limit, params?.before]
4053
4076
  );
4077
+ const [olderMessages, setOlderMessages] = useState15([]);
4078
+ const [hasMoreOlderMessages, setHasMoreOlderMessages] = useState15(true);
4079
+ const [loadingOlderMessages, setLoadingOlderMessages] = useState15(false);
4080
+ const loadingOlderMessagesRef = useRef7(false);
4081
+ useEffect10(() => {
4082
+ setOlderMessages([]);
4083
+ setHasMoreOlderMessages(true);
4084
+ }, [conversationId]);
4085
+ const messages = useMemo3(() => dedupeMessagesById([...olderMessages, ...data ?? []]), [olderMessages, data]);
4086
+ const loadOlderMessages = useCallback7(async () => {
4087
+ const oldestMessage = messages[0];
4088
+ if (!shouldFetchOlderMessages({
4089
+ isLoading: loadingOlderMessagesRef.current,
4090
+ hasMore: hasMoreOlderMessages,
4091
+ oldestMessage
4092
+ }))
4093
+ return;
4094
+ if (!oldestMessage) return;
4095
+ loadingOlderMessagesRef.current = true;
4096
+ setLoadingOlderMessages(true);
4097
+ try {
4098
+ const olderPage = await api.fetchMessages(conversationId, {
4099
+ limit: OLDER_MESSAGES_PAGE_LIMIT,
4100
+ before: oldestMessage.timestamp
4101
+ });
4102
+ if (!pageIndicatesMoreOlderMessages(olderPage.length, OLDER_MESSAGES_PAGE_LIMIT)) setHasMoreOlderMessages(false);
4103
+ setOlderMessages((current) => prependOlderMessages(current, olderPage));
4104
+ } finally {
4105
+ loadingOlderMessagesRef.current = false;
4106
+ setLoadingOlderMessages(false);
4107
+ }
4108
+ }, [api, conversationId, hasMoreOlderMessages, messages]);
4054
4109
  const sendMessage = useCallback7(
4055
4110
  async (text) => {
4056
4111
  const message = await api.sendMessage(conversationId, text);
@@ -4075,20 +4130,32 @@ function useConversationMessages(conversationId, params) {
4075
4130
  [api, conversationId, refetch]
4076
4131
  );
4077
4132
  const markRead = useCallback7(() => api.markRead(conversationId), [api, conversationId]);
4078
- return { messages: data ?? [], loading, error, refetch, sendMessage, sendMedia, sendTemplate, markRead };
4133
+ return {
4134
+ messages,
4135
+ loading,
4136
+ error,
4137
+ refetch,
4138
+ sendMessage,
4139
+ sendMedia,
4140
+ sendTemplate,
4141
+ markRead,
4142
+ loadOlderMessages,
4143
+ loadingOlderMessages,
4144
+ hasMoreOlderMessages
4145
+ };
4079
4146
  }
4080
4147
 
4081
4148
  // src/hooks/useScrollToLatestMessage.ts
4082
- import { useCallback as useCallback8, useLayoutEffect, useRef as useRef7, useState as useState15 } from "react";
4149
+ import { useCallback as useCallback8, useLayoutEffect, useRef as useRef8, useState as useState16 } from "react";
4083
4150
  var NEAR_BOTTOM_THRESHOLD_PX = 120;
4084
4151
  var SCROLL_BEHAVIOR = "auto";
4085
4152
  function useScrollToLatestMessage({
4086
4153
  conversationId,
4087
4154
  messageCount
4088
4155
  }) {
4089
- const containerRef = useRef7(null);
4090
- const [isAwayFromBottom, setIsAwayFromBottom] = useState15(false);
4091
- const isAwayFromBottomRef = useRef7(false);
4156
+ const containerRef = useRef8(null);
4157
+ const [isAwayFromBottom, setIsAwayFromBottom] = useState16(false);
4158
+ const isAwayFromBottomRef = useRef8(false);
4092
4159
  const scrollToBottom = useCallback8((behavior = SCROLL_BEHAVIOR) => {
4093
4160
  const container = containerRef.current;
4094
4161
  if (!container) return;
@@ -4101,7 +4168,7 @@ function useScrollToLatestMessage({
4101
4168
  isAwayFromBottomRef.current = away;
4102
4169
  setIsAwayFromBottom((current) => current === away ? current : away);
4103
4170
  }, []);
4104
- const jumpedForConversationRef = useRef7(void 0);
4171
+ const jumpedForConversationRef = useRef8(void 0);
4105
4172
  useLayoutEffect(() => {
4106
4173
  const isNewConversation = jumpedForConversationRef.current !== conversationId;
4107
4174
  if (isNewConversation) {
@@ -4148,12 +4215,12 @@ function useConversationContext(conversationId) {
4148
4215
  }
4149
4216
 
4150
4217
  // src/hooks/useConversationRealtime.ts
4151
- import { useEffect as useEffect10, useRef as useRef8 } from "react";
4218
+ import { useEffect as useEffect11, useRef as useRef9 } from "react";
4152
4219
  function useConversationRealtime(conversationId, onEvent) {
4153
4220
  const sse = useConversations()?.sse;
4154
- const onEventRef = useRef8(onEvent);
4221
+ const onEventRef = useRef9(onEvent);
4155
4222
  onEventRef.current = onEvent;
4156
- useEffect10(() => {
4223
+ useEffect11(() => {
4157
4224
  if (!sse || !conversationId) return;
4158
4225
  const source = sse.connectConversationStream(conversationId);
4159
4226
  const handler = (event) => onEventRef.current(event);
@@ -4166,9 +4233,9 @@ function useConversationRealtime(conversationId, onEvent) {
4166
4233
  }
4167
4234
  function useGlobalRealtime(onEvent) {
4168
4235
  const sse = useConversations()?.sse;
4169
- const onEventRef = useRef8(onEvent);
4236
+ const onEventRef = useRef9(onEvent);
4170
4237
  onEventRef.current = onEvent;
4171
- useEffect10(() => {
4238
+ useEffect11(() => {
4172
4239
  if (!sse) return;
4173
4240
  const source = sse.connectGlobalStream();
4174
4241
  const handler = (event) => onEventRef.current(event);
@@ -4181,14 +4248,14 @@ function useGlobalRealtime(onEvent) {
4181
4248
  }
4182
4249
 
4183
4250
  // src/hooks/useConversationActions.ts
4184
- import { useMemo as useMemo3 } from "react";
4251
+ import { useMemo as useMemo4 } from "react";
4185
4252
  function useConversationActions(conversationId) {
4186
4253
  const context = useConversations();
4187
4254
  if (!context) {
4188
4255
  throw new Error("useConversationActions requires an ancestor <ConversationsProvider>");
4189
4256
  }
4190
4257
  const { api } = context;
4191
- return useMemo3(
4258
+ return useMemo4(
4192
4259
  () => ({
4193
4260
  takeover: api.takeover ? () => api.takeover(conversationId) : void 0,
4194
4261
  release: api.release ? () => api.release(conversationId) : void 0,
@@ -4203,7 +4270,7 @@ function useInboxActions() {
4203
4270
  throw new Error("useInboxActions requires an ancestor <ConversationsProvider>");
4204
4271
  }
4205
4272
  const { api } = context;
4206
- return useMemo3(
4273
+ return useMemo4(
4207
4274
  () => ({
4208
4275
  markAllRead: api.markAllRead ? () => api.markAllRead() : void 0,
4209
4276
  listTemplates: api.listTemplates ? () => api.listTemplates() : void 0
@@ -4347,11 +4414,11 @@ async function sendQueuedMessage(params) {
4347
4414
  }
4348
4415
 
4349
4416
  // src/quickReplies/QuickRepliesWorkspace.tsx
4350
- import { useEffect as useEffect13, useId as useId2, useRef as useRef11, useState as useState18 } from "react";
4417
+ import { useEffect as useEffect14, useId as useId2, useRef as useRef12, useState as useState19 } from "react";
4351
4418
  import { Pencil as Pencil2, Plus as Plus2, Trash2 as Trash23 } from "lucide-react";
4352
4419
 
4353
4420
  // src/quickReplies/useQuickRepliesWorkspace.ts
4354
- import { useCallback as useCallback10, useEffect as useEffect12, useState as useState17 } from "react";
4421
+ import { useCallback as useCallback10, useEffect as useEffect13, useState as useState18 } from "react";
4355
4422
 
4356
4423
  // src/quickReplies/quickReplyAttachmentUpload.ts
4357
4424
  function validateAttachmentFiles(files, currentCount, limits) {
@@ -4383,7 +4450,7 @@ function moveAttachment(list, index, direction) {
4383
4450
  }
4384
4451
 
4385
4452
  // src/quickReplies/useQuickReplyAttachmentUploads.ts
4386
- import { useCallback as useCallback9, useEffect as useEffect11, useRef as useRef9, useState as useState16 } from "react";
4453
+ import { useCallback as useCallback9, useEffect as useEffect12, useRef as useRef10, useState as useState17 } from "react";
4387
4454
 
4388
4455
  // src/quickReplies/createUploadQueue.ts
4389
4456
  function createUploadQueue(maxConcurrent) {
@@ -4439,11 +4506,11 @@ function useQuickReplyAttachmentUploads({
4439
4506
  attachmentsCount,
4440
4507
  onUploaded
4441
4508
  }) {
4442
- const [pendingUploads, setPendingUploads] = useState16([]);
4443
- const [attachmentRejections, setAttachmentRejections] = useState16([]);
4444
- const uploadQueueRef = useRef9(createUploadQueue(3));
4445
- const isMountedRef = useRef9(true);
4446
- useEffect11(
4509
+ const [pendingUploads, setPendingUploads] = useState17([]);
4510
+ const [attachmentRejections, setAttachmentRejections] = useState17([]);
4511
+ const uploadQueueRef = useRef10(createUploadQueue(3));
4512
+ const isMountedRef = useRef10(true);
4513
+ useEffect12(
4447
4514
  () => () => {
4448
4515
  isMountedRef.current = false;
4449
4516
  uploadQueueRef.current.abortAll();
@@ -4621,15 +4688,15 @@ function useQuickRepliesWorkspace({
4621
4688
  labels,
4622
4689
  attachmentSizeLimits
4623
4690
  }) {
4624
- const [quickReplies, setQuickReplies] = useState17([]);
4625
- const [isLoading, setIsLoading] = useState17(false);
4626
- const [loadError, setLoadError] = useState17(void 0);
4627
- const [search, setSearch] = useState17("");
4628
- const [editing, setEditing] = useState17(void 0);
4629
- const [fieldErrors, setFieldErrors] = useState17({});
4630
- const [isSaving, setIsSaving] = useState17(false);
4631
- const [saveError, setSaveError] = useState17(void 0);
4632
- const [deletingId, setDeletingId] = useState17(void 0);
4691
+ const [quickReplies, setQuickReplies] = useState18([]);
4692
+ const [isLoading, setIsLoading] = useState18(false);
4693
+ const [loadError, setLoadError] = useState18(void 0);
4694
+ const [search, setSearch] = useState18("");
4695
+ const [editing, setEditing] = useState18(void 0);
4696
+ const [fieldErrors, setFieldErrors] = useState18({});
4697
+ const [isSaving, setIsSaving] = useState18(false);
4698
+ const [saveError, setSaveError] = useState18(void 0);
4699
+ const [deletingId, setDeletingId] = useState18(void 0);
4633
4700
  const uploadAttachment = useCallback10((attachment) => {
4634
4701
  setEditing((current) => current ? { ...current, attachments: [...current.attachments, attachment] } : current);
4635
4702
  }, []);
@@ -4648,7 +4715,7 @@ function useQuickRepliesWorkspace({
4648
4715
  attachmentsCount: editing?.attachments.length ?? 0,
4649
4716
  onUploaded: uploadAttachment
4650
4717
  });
4651
- useEffect12(() => {
4718
+ useEffect13(() => {
4652
4719
  if (!api.listQuickReplies) return;
4653
4720
  let cancelled = false;
4654
4721
  setIsLoading(true);
@@ -4976,7 +5043,7 @@ function QuickReplyWhatsAppPreview({ body, variables, attachments, labels }) {
4976
5043
  }
4977
5044
 
4978
5045
  // src/quickReplies/AttachmentsFormSection.tsx
4979
- import { useRef as useRef10 } from "react";
5046
+ import { useRef as useRef11 } from "react";
4980
5047
  import { Paperclip as Paperclip3, X as X4 } from "lucide-react";
4981
5048
  import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
4982
5049
  function AttachmentsFormSection({
@@ -4991,7 +5058,7 @@ function AttachmentsFormSection({
4991
5058
  onMoveAttachment,
4992
5059
  onDismissRejections
4993
5060
  }) {
4994
- const attachmentFileInputRef = useRef10(null);
5061
+ const attachmentFileInputRef = useRef11(null);
4995
5062
  return /* @__PURE__ */ jsxs24("div", { className: "space-y-2", children: [
4996
5063
  /* @__PURE__ */ jsx27("span", { className: "block text-sm font-medium", children: text.attachmentsTitle }),
4997
5064
  attachments.length === 0 && pendingUploads.length === 0 ? /* @__PURE__ */ jsx27("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: text.attachmentsEmpty }) : /* @__PURE__ */ jsxs24("ul", { className: "space-y-1", children: [
@@ -5126,15 +5193,15 @@ function QuickRepliesWorkspace({
5126
5193
  attachmentSizeLimits
5127
5194
  }) {
5128
5195
  const text = { ...DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS, ...labels };
5129
- const bodyFieldRef = useRef11(null);
5196
+ const bodyFieldRef = useRef12(null);
5130
5197
  const formId = useId2();
5131
5198
  const titleFieldId = `${formId}-title`;
5132
5199
  const shortcutFieldId = `${formId}-shortcut`;
5133
5200
  const bodyFieldId = `${formId}-body`;
5134
- const [confirmingDeleteId, setConfirmingDeleteId] = useState18(null);
5135
- const deleteButtonRefs = useRef11({});
5136
- const confirmButtonRef = useRef11(null);
5137
- useEffect13(() => {
5201
+ const [confirmingDeleteId, setConfirmingDeleteId] = useState19(null);
5202
+ const deleteButtonRefs = useRef12({});
5203
+ const confirmButtonRef = useRef12(null);
5204
+ useEffect14(() => {
5138
5205
  if (confirmingDeleteId) confirmButtonRef.current?.focus();
5139
5206
  }, [confirmingDeleteId]);
5140
5207
  const cancelDeleteConfirm = (id) => {
@@ -5469,11 +5536,11 @@ function QuickRepliesWorkspace({
5469
5536
  }
5470
5537
 
5471
5538
  // src/workspace/ConversationsWorkspace.tsx
5472
- import { useEffect as useEffect19, useMemo as useMemo6, useState as useState25 } from "react";
5539
+ import { useEffect as useEffect20, useMemo as useMemo7, useState as useState26 } from "react";
5473
5540
  import { Check as Check2, CheckCheck, FlaskConical, Hourglass as Hourglass2, Mail as Mail2, MessagesSquare } from "lucide-react";
5474
5541
 
5475
5542
  // src/workspace/BulkTemplateModal.tsx
5476
- import { useEffect as useEffect14, useMemo as useMemo4, useState as useState19 } from "react";
5543
+ import { useEffect as useEffect15, useMemo as useMemo5, useState as useState20 } from "react";
5477
5544
  import { jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
5478
5545
  function BulkTemplateModal({
5479
5546
  labels,
@@ -5487,10 +5554,10 @@ function BulkTemplateModal({
5487
5554
  throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
5488
5555
  }
5489
5556
  const { api } = context;
5490
- const [templates, setTemplates] = useState19([]);
5491
- const [search, setSearch] = useState19("");
5492
- const [selected, setSelected] = useState19("");
5493
- useEffect14(() => {
5557
+ const [templates, setTemplates] = useState20([]);
5558
+ const [search, setSearch] = useState20("");
5559
+ const [selected, setSelected] = useState20("");
5560
+ useEffect15(() => {
5494
5561
  let active = true;
5495
5562
  void api.listTemplates?.().then((loaded) => {
5496
5563
  if (active) setTemplates(loaded);
@@ -5500,7 +5567,7 @@ function BulkTemplateModal({
5500
5567
  active = false;
5501
5568
  };
5502
5569
  }, [api]);
5503
- const filtered = useMemo4(() => {
5570
+ const filtered = useMemo5(() => {
5504
5571
  const term = search.trim().toLowerCase();
5505
5572
  if (!term) return templates;
5506
5573
  return templates.filter((template) => template.name.toLowerCase().includes(term));
@@ -5564,7 +5631,48 @@ function BulkTemplateModal({
5564
5631
  }
5565
5632
 
5566
5633
  // src/workspace/ConversationPane.tsx
5567
- import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef15, useState as useState23 } from "react";
5634
+ import { useCallback as useCallback14, useEffect as useEffect18, useRef as useRef17, useState as useState24 } from "react";
5635
+
5636
+ // src/hooks/useLoadOlderMessagesScroll.ts
5637
+ import { useCallback as useCallback11, useLayoutEffect as useLayoutEffect2, useRef as useRef13 } from "react";
5638
+ var NEAR_TOP_THRESHOLD_PX = 200;
5639
+ function computeScrollTopAfterPrepend(before, scrollHeightAfter) {
5640
+ return before.scrollTop + (scrollHeightAfter - before.scrollHeight);
5641
+ }
5642
+ function useLoadOlderMessagesScroll({
5643
+ containerRef,
5644
+ oldestMessageId,
5645
+ hasMore,
5646
+ loading,
5647
+ onLoadOlder
5648
+ }) {
5649
+ const pendingSnapshotRef = useRef13(null);
5650
+ const previousOldestMessageIdRef = useRef13(oldestMessageId);
5651
+ const triggerLoadOlder = useCallback11(() => {
5652
+ if (loading || !hasMore) return;
5653
+ const container = containerRef.current;
5654
+ if (!container) return;
5655
+ pendingSnapshotRef.current = { scrollHeight: container.scrollHeight, scrollTop: container.scrollTop };
5656
+ onLoadOlder();
5657
+ }, [containerRef, hasMore, loading, onLoadOlder]);
5658
+ const handleScroll = useCallback11(
5659
+ (event) => {
5660
+ if (event.currentTarget.scrollTop > NEAR_TOP_THRESHOLD_PX) return;
5661
+ triggerLoadOlder();
5662
+ },
5663
+ [triggerLoadOlder]
5664
+ );
5665
+ useLayoutEffect2(() => {
5666
+ const idChanged = previousOldestMessageIdRef.current !== oldestMessageId;
5667
+ previousOldestMessageIdRef.current = oldestMessageId;
5668
+ const container = containerRef.current;
5669
+ const pending = pendingSnapshotRef.current;
5670
+ if (!idChanged || !container || !pending) return;
5671
+ pendingSnapshotRef.current = null;
5672
+ container.scrollTop = computeScrollTopAfterPrepend(pending, container.scrollHeight);
5673
+ }, [oldestMessageId, containerRef]);
5674
+ return { handleScroll, triggerLoadOlder };
5675
+ }
5568
5676
 
5569
5677
  // src/quickReplies/resolveConversationVariables.ts
5570
5678
  var MARKER_PATTERN = /^\{\{\s*([\w.]+)\s*\}\}$/;
@@ -5585,7 +5693,7 @@ function resolveConversationVariables({
5585
5693
  }
5586
5694
 
5587
5695
  // src/workspace/QueuedAttachmentsList.tsx
5588
- import { useEffect as useEffect15, useRef as useRef12, useState as useState20 } from "react";
5696
+ import { useEffect as useEffect16, useRef as useRef14, useState as useState21 } from "react";
5589
5697
  import { jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
5590
5698
  function nameOf(item) {
5591
5699
  return item.kind === "local" ? item.file.name : item.filename;
@@ -5601,16 +5709,16 @@ function AttachmentThumbnail({
5601
5709
  getThumbnailUrl
5602
5710
  }) {
5603
5711
  const isImage = mimeTypeOf(item).startsWith("image/");
5604
- const [url, setUrl] = useState20(item.kind === "local" ? void 0 : item.previewUrl);
5712
+ const [url, setUrl] = useState21(item.kind === "local" ? void 0 : item.previewUrl);
5605
5713
  const localFile = item.kind === "local" ? item.file : void 0;
5606
5714
  const uploadId = item.kind === "stored" ? item.uploadId : void 0;
5607
- useEffect15(() => {
5715
+ useEffect16(() => {
5608
5716
  if (!isImage || !localFile) return;
5609
5717
  const objectUrl = URL.createObjectURL(localFile);
5610
5718
  setUrl(objectUrl);
5611
5719
  return () => URL.revokeObjectURL(objectUrl);
5612
5720
  }, [isImage, localFile]);
5613
- useEffect15(() => {
5721
+ useEffect16(() => {
5614
5722
  if (!isImage || !uploadId || !getThumbnailUrl) return;
5615
5723
  let cancelled = false;
5616
5724
  getThumbnailUrl(uploadId).then((resolved) => {
@@ -5628,11 +5736,11 @@ function departedItemsOf(previouslyRendered, items) {
5628
5736
  return previouslyRendered.filter((item) => !currentKeys.has(attachmentKey(item)));
5629
5737
  }
5630
5738
  function useDepartingItems(items) {
5631
- const [rendered, setRendered] = useState20(items);
5632
- const [departingKeys, setDepartingKeys] = useState20(/* @__PURE__ */ new Set());
5633
- const renderedRef = useRef12(rendered);
5739
+ const [rendered, setRendered] = useState21(items);
5740
+ const [departingKeys, setDepartingKeys] = useState21(/* @__PURE__ */ new Set());
5741
+ const renderedRef = useRef14(rendered);
5634
5742
  renderedRef.current = rendered;
5635
- useEffect15(() => {
5743
+ useEffect16(() => {
5636
5744
  const removedItems = departedItemsOf(renderedRef.current, items);
5637
5745
  if (removedItems.length === 0) {
5638
5746
  setRendered(items);
@@ -5716,10 +5824,10 @@ function QueuedAttachmentsList({
5716
5824
  }
5717
5825
 
5718
5826
  // src/workspace/useComposerQueue.ts
5719
- import { useCallback as useCallback12, useEffect as useEffect16, useRef as useRef14, useState as useState22 } from "react";
5827
+ import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef16, useState as useState23 } from "react";
5720
5828
 
5721
5829
  // src/workspace/useComposerAttachmentRetry.ts
5722
- import { useCallback as useCallback11, useRef as useRef13, useState as useState21 } from "react";
5830
+ import { useCallback as useCallback12, useRef as useRef15, useState as useState22 } from "react";
5723
5831
  function useComposerAttachmentRetry(params) {
5724
5832
  const {
5725
5833
  conversationId,
@@ -5733,9 +5841,9 @@ function useComposerAttachmentRetry(params) {
5733
5841
  labels,
5734
5842
  sendInFlightRef
5735
5843
  } = params;
5736
- const retryIdempotencyKeyRef = useRef13(void 0);
5737
- const retryingKeysRef = useRef13(/* @__PURE__ */ new Set());
5738
- const [retryingKeys, setRetryingKeys] = useState21(/* @__PURE__ */ new Set());
5844
+ const retryIdempotencyKeyRef = useRef15(void 0);
5845
+ const retryingKeysRef = useRef15(/* @__PURE__ */ new Set());
5846
+ const [retryingKeys, setRetryingKeys] = useState22(/* @__PURE__ */ new Set());
5739
5847
  function markRetrying(key) {
5740
5848
  retryingKeysRef.current.add(key);
5741
5849
  setRetryingKeys(new Set(retryingKeysRef.current));
@@ -5811,7 +5919,7 @@ function useComposerAttachmentRetry(params) {
5811
5919
  }
5812
5920
  void retryStoredAttachment(item).finally(release);
5813
5921
  }
5814
- const resetRetryState = useCallback11(() => {
5922
+ const resetRetryState = useCallback12(() => {
5815
5923
  retryIdempotencyKeyRef.current = void 0;
5816
5924
  retryingKeysRef.current.clear();
5817
5925
  setRetryingKeys(/* @__PURE__ */ new Set());
@@ -5822,12 +5930,12 @@ function useComposerAttachmentRetry(params) {
5822
5930
  // src/workspace/useComposerQueue.ts
5823
5931
  function useComposerQueue(params) {
5824
5932
  const { conversationId, draft, setDraft, api, labels, onSendAttachments, refetch, setSendFailure } = params;
5825
- const [queue, setQueue] = useState22([]);
5826
- const [attachmentStatus, setAttachmentStatus] = useState22({});
5827
- const [isSendingDraft, setIsSendingDraft] = useState22(false);
5828
- const sendInFlightRef = useRef14(false);
5829
- const idempotencyKeyRef = useRef14(void 0);
5830
- const currentConversationIdRef = useRef14(conversationId);
5933
+ const [queue, setQueue] = useState23([]);
5934
+ const [attachmentStatus, setAttachmentStatus] = useState23({});
5935
+ const [isSendingDraft, setIsSendingDraft] = useState23(false);
5936
+ const sendInFlightRef = useRef16(false);
5937
+ const idempotencyKeyRef = useRef16(void 0);
5938
+ const currentConversationIdRef = useRef16(conversationId);
5831
5939
  const { retryQueuedAttachment, resetRetryState, retryingKeys } = useComposerAttachmentRetry({
5832
5940
  conversationId,
5833
5941
  currentConversationIdRef,
@@ -5840,7 +5948,7 @@ function useComposerQueue(params) {
5840
5948
  sendInFlightRef,
5841
5949
  ...onSendAttachments ? { onSendAttachments } : {}
5842
5950
  });
5843
- useEffect16(() => {
5951
+ useEffect17(() => {
5844
5952
  currentConversationIdRef.current = conversationId;
5845
5953
  setQueue([]);
5846
5954
  setAttachmentStatus({});
@@ -5849,11 +5957,11 @@ function useComposerQueue(params) {
5849
5957
  setIsSendingDraft(false);
5850
5958
  resetRetryState();
5851
5959
  }, [conversationId, resetRetryState]);
5852
- const enqueueAttachments = useCallback12((items) => {
5960
+ const enqueueAttachments = useCallback13((items) => {
5853
5961
  if (items.length === 0) return;
5854
5962
  setQueue((current) => [...current, ...items]);
5855
5963
  }, []);
5856
- const removeQueuedAttachment = useCallback12((item) => {
5964
+ const removeQueuedAttachment = useCallback13((item) => {
5857
5965
  setQueue((current) => current.filter((queued) => attachmentKey(queued) !== attachmentKey(item)));
5858
5966
  }, []);
5859
5967
  async function runSend(action, fallback) {
@@ -5991,12 +6099,14 @@ function ConversationPane({
5991
6099
  throw new Error("ConversationPane requires an ancestor <ConversationsProvider>");
5992
6100
  }
5993
6101
  const { api } = context;
5994
- const { messages, refetch } = useConversationMessages(conversation.id);
6102
+ const { messages, refetch, loadOlderMessages, loadingOlderMessages, hasMoreOlderMessages } = useConversationMessages(
6103
+ conversation.id
6104
+ );
5995
6105
  const { context: conversationContext } = useConversationContext(conversation.id);
5996
- const [documentsOpen, setDocumentsOpen] = useState23(false);
5997
- const [sendFailure, setSendFailure] = useState23(void 0);
5998
- const [selectedMessageIds, setSelectedMessageIds] = useState23(/* @__PURE__ */ new Set());
5999
- const [draft, setDraft] = useState23(initialComposerText ?? "");
6106
+ const [documentsOpen, setDocumentsOpen] = useState24(false);
6107
+ const [sendFailure, setSendFailure] = useState24(void 0);
6108
+ const [selectedMessageIds, setSelectedMessageIds] = useState24(/* @__PURE__ */ new Set());
6109
+ const [draft, setDraft] = useState24(initialComposerText ?? "");
6000
6110
  const {
6001
6111
  queue,
6002
6112
  enqueueAttachments,
@@ -6016,8 +6126,8 @@ function ConversationPane({
6016
6126
  refetch,
6017
6127
  setSendFailure
6018
6128
  });
6019
- const thumbnailUrlCacheRef = useRef15(/* @__PURE__ */ new Map());
6020
- const getQueuedAttachmentThumbnailUrl = useCallback13(
6129
+ const thumbnailUrlCacheRef = useRef17(/* @__PURE__ */ new Map());
6130
+ const getQueuedAttachmentThumbnailUrl = useCallback14(
6021
6131
  (uploadId) => {
6022
6132
  const cached = thumbnailUrlCacheRef.current.get(uploadId);
6023
6133
  if (cached) return cached;
@@ -6028,7 +6138,7 @@ function ConversationPane({
6028
6138
  },
6029
6139
  [api]
6030
6140
  );
6031
- useEffect17(() => {
6141
+ useEffect18(() => {
6032
6142
  setSelectedMessageIds(/* @__PURE__ */ new Set());
6033
6143
  setDraft(initialComposerText ?? "");
6034
6144
  }, [conversation.id, initialComposerText]);
@@ -6048,6 +6158,13 @@ function ConversationPane({
6048
6158
  setSelectedMessageIds(/* @__PURE__ */ new Set());
6049
6159
  }
6050
6160
  const scroll = useScrollToLatestMessage({ conversationId: conversation.id, messageCount: messages.length });
6161
+ const loadOlder = useLoadOlderMessagesScroll({
6162
+ containerRef: scroll.containerRef,
6163
+ oldestMessageId: messages[0]?.id,
6164
+ hasMore: hasMoreOlderMessages,
6165
+ loading: loadingOlderMessages,
6166
+ onLoadOlder: () => void loadOlderMessages()
6167
+ });
6051
6168
  useConversationRealtime(conversation.id, () => {
6052
6169
  void refetch();
6053
6170
  });
@@ -6131,32 +6248,38 @@ function ConversationPane({
6131
6248
  contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx31(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
6132
6249
  /* @__PURE__ */ jsx31(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
6133
6250
  renderAboveTranscript?.(conversation, conversationContext),
6134
- /* @__PURE__ */ jsx31(
6251
+ /* @__PURE__ */ jsxs28(
6135
6252
  ConversationWallpaper,
6136
6253
  {
6137
6254
  ref: scroll.containerRef,
6138
- onScroll: scroll.handleScroll,
6255
+ onScroll: (event) => {
6256
+ scroll.handleScroll(event);
6257
+ loadOlder.handleScroll(event);
6258
+ },
6139
6259
  className: "cv-workspace-transcript",
6140
- children: messages.map((message, index) => {
6141
- const previous = index > 0 ? messages[index - 1] : void 0;
6142
- const startsNewDay = !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString();
6143
- return /* @__PURE__ */ jsxs28("div", { children: [
6144
- startsNewDay ? /* @__PURE__ */ jsx31(DateDivider, { iso: message.timestamp }) : null,
6145
- /* @__PURE__ */ jsx31(
6146
- MessageBubble,
6147
- {
6148
- message,
6149
- isMine: message.direction === "outbound",
6150
- isFirstInGroup: !previous || previous.sender !== message.sender,
6151
- ...messageSelection ? {
6152
- isSelecting: selectedMessageIds.size > 0,
6153
- isSelected: selectedMessageIds.has(message.id),
6154
- onToggleSelect: () => toggleMessageSelected(message.id)
6155
- } : {}
6156
- }
6157
- )
6158
- ] }, message.id);
6159
- })
6260
+ children: [
6261
+ hasMoreOlderMessages ? /* @__PURE__ */ jsx31("div", { className: "cv-workspace-load-older", children: /* @__PURE__ */ jsx31("button", { type: "button", onClick: loadOlder.triggerLoadOlder, disabled: loadingOlderMessages, children: loadingOlderMessages ? labels.loadingOlderMessages : labels.loadOlderMessages }) }) : null,
6262
+ messages.map((message, index) => {
6263
+ const previous = index > 0 ? messages[index - 1] : void 0;
6264
+ const startsNewDay = !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString();
6265
+ return /* @__PURE__ */ jsxs28("div", { children: [
6266
+ startsNewDay ? /* @__PURE__ */ jsx31(DateDivider, { iso: message.timestamp }) : null,
6267
+ /* @__PURE__ */ jsx31(
6268
+ MessageBubble,
6269
+ {
6270
+ message,
6271
+ isMine: message.direction === "outbound",
6272
+ isFirstInGroup: !previous || previous.sender !== message.sender,
6273
+ ...messageSelection ? {
6274
+ isSelecting: selectedMessageIds.size > 0,
6275
+ isSelected: selectedMessageIds.has(message.id),
6276
+ onToggleSelect: () => toggleMessageSelected(message.id)
6277
+ } : {}
6278
+ }
6279
+ )
6280
+ ] }, message.id);
6281
+ })
6282
+ ]
6160
6283
  }
6161
6284
  ),
6162
6285
  messageSelection && selectedMessageIds.size > 0 ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-selection", children: [
@@ -6196,9 +6319,11 @@ function ConversationPane({
6196
6319
  onSend: () => void handleRichSend(),
6197
6320
  ...onSendAttachments ? {
6198
6321
  onAttachFiles: (files) => enqueueAttachments(
6199
- Array.from(files).map(
6200
- (file) => ({ kind: "local", localId: crypto.randomUUID(), file })
6201
- )
6322
+ Array.from(files).map((file) => ({
6323
+ kind: "local",
6324
+ localId: crypto.randomUUID(),
6325
+ file
6326
+ }))
6202
6327
  )
6203
6328
  } : onAttach ? {
6204
6329
  onAttachFiles: (files) => {
@@ -6444,6 +6569,8 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
6444
6569
  templateModalSend: (count) => `Enviar para ${count}`,
6445
6570
  messagesSelected: (count) => `${count} mensagem${count === 1 ? "" : "s"} selecionada${count === 1 ? "" : "s"}`,
6446
6571
  copySelected: "Copiar",
6572
+ loadOlderMessages: "Carregar mensagens anteriores",
6573
+ loadingOlderMessages: "Carregando\u2026",
6447
6574
  composerPlaceholder: "Responder como atendente\u2026",
6448
6575
  attachFailure: "Falha ao enviar o arquivo.",
6449
6576
  attachmentRemove: "Remover anexo",
@@ -6462,7 +6589,7 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
6462
6589
  };
6463
6590
 
6464
6591
  // src/workspace/useConversationsInbox.ts
6465
- import { useCallback as useCallback14, useEffect as useEffect18, useMemo as useMemo5, useRef as useRef16, useState as useState24 } from "react";
6592
+ import { useCallback as useCallback15, useEffect as useEffect19, useMemo as useMemo6, useRef as useRef18, useState as useState25 } from "react";
6466
6593
  var CONVERSATIONS_PER_PAGE = 50;
6467
6594
  function defaultDescribeFailure(error) {
6468
6595
  const status = error.status;
@@ -6480,14 +6607,14 @@ function useConversationsInbox(params = {}) {
6480
6607
  const perPage = params.perPage ?? CONVERSATIONS_PER_PAGE;
6481
6608
  const markReadOnOpen = params.markReadOnOpen ?? true;
6482
6609
  const describeFailure = params.describeFailure ?? defaultDescribeFailure;
6483
- const [waitingOnly, setWaitingOnly] = useState24(false);
6484
- const [windowFilter, setWindowFilter] = useState24(CONVERSATION_WINDOW.ALL);
6485
- const [channelFilter, setChannelFilter] = useState24(CHANNEL_FILTER_ALL);
6486
- const [search, setSearch] = useState24("");
6487
- const [page, setPage] = useState24(1);
6488
- const [selectedId, setSelectedId] = useState24(void 0);
6489
- const [selectedIds, setSelectedIds] = useState24(/* @__PURE__ */ new Set());
6490
- const [busy, setBusy] = useState24(false);
6610
+ const [waitingOnly, setWaitingOnly] = useState25(false);
6611
+ const [windowFilter, setWindowFilter] = useState25(CONVERSATION_WINDOW.ALL);
6612
+ const [channelFilter, setChannelFilter] = useState25(CHANNEL_FILTER_ALL);
6613
+ const [search, setSearch] = useState25("");
6614
+ const [page, setPage] = useState25(1);
6615
+ const [selectedId, setSelectedId] = useState25(void 0);
6616
+ const [selectedIds, setSelectedIds] = useState25(/* @__PURE__ */ new Set());
6617
+ const [busy, setBusy] = useState25(false);
6491
6618
  const { conversations, total, loading, error, refetch } = useConversationList({
6492
6619
  ...waitingOnly ? { waitingHuman: true } : {},
6493
6620
  ...search ? { search } : {},
@@ -6495,12 +6622,12 @@ function useConversationsInbox(params = {}) {
6495
6622
  ...params.serverPaginated ? { page, limit: perPage } : {}
6496
6623
  });
6497
6624
  useGlobalRealtime(
6498
- useCallback14(() => {
6625
+ useCallback15(() => {
6499
6626
  void refetch();
6500
6627
  }, [refetch])
6501
6628
  );
6502
- const now = useMemo5(() => Date.now(), [conversations]);
6503
- const filtered = useMemo5(
6629
+ const now = useMemo6(() => Date.now(), [conversations]);
6630
+ const filtered = useMemo6(
6504
6631
  () => conversations.filter(
6505
6632
  (conversation) => channelFilter === CHANNEL_FILTER_ALL || (conversation.channel ?? DEFAULT_CONVERSATION_CHANNEL) === channelFilter
6506
6633
  ).filter(
@@ -6512,16 +6639,16 @@ function useConversationsInbox(params = {}) {
6512
6639
  const pageCount = Math.max(1, Math.ceil(totalForPaging / perPage));
6513
6640
  const currentPage = Math.min(page, pageCount);
6514
6641
  const pageConversations = params.serverPaginated ? filtered : filtered.slice((currentPage - 1) * perPage, currentPage * perPage);
6515
- useEffect18(() => {
6642
+ useEffect19(() => {
6516
6643
  setPage(1);
6517
6644
  }, [search, waitingOnly, windowFilter, channelFilter]);
6518
- useEffect18(() => {
6645
+ useEffect19(() => {
6519
6646
  if (selectedId && !conversations.some((conversation) => conversation.id === selectedId)) {
6520
6647
  setSelectedId(void 0);
6521
6648
  }
6522
6649
  }, [conversations, selectedId]);
6523
- const lastMarkReadAttempt = useRef16(void 0);
6524
- useEffect18(() => {
6650
+ const lastMarkReadAttempt = useRef18(void 0);
6651
+ useEffect19(() => {
6525
6652
  if (!markReadOnOpen || !selectedId) return;
6526
6653
  const opened = conversations.find((conversation) => conversation.id === selectedId);
6527
6654
  if (!opened || opened.unread === 0) return;
@@ -6531,7 +6658,7 @@ function useConversationsInbox(params = {}) {
6531
6658
  void api.markRead(selectedId).then(() => refetch()).catch(() => {
6532
6659
  });
6533
6660
  }, [markReadOnOpen, selectedId, conversations, refetch, api]);
6534
- const toggleSelected = useCallback14((conversationId) => {
6661
+ const toggleSelected = useCallback15((conversationId) => {
6535
6662
  setSelectedIds((current) => {
6536
6663
  const next = new Set(current);
6537
6664
  if (next.has(conversationId)) next.delete(conversationId);
@@ -6541,7 +6668,7 @@ function useConversationsInbox(params = {}) {
6541
6668
  }, []);
6542
6669
  const pageIds = pageConversations.map((conversation) => conversation.id);
6543
6670
  const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
6544
- const toggleSelectAllOnPage = useCallback14(() => {
6671
+ const toggleSelectAllOnPage = useCallback15(() => {
6545
6672
  setSelectedIds((current) => {
6546
6673
  const next = new Set(current);
6547
6674
  if (allOnPageSelected) pageIds.forEach((id) => next.delete(id));
@@ -6549,7 +6676,7 @@ function useConversationsInbox(params = {}) {
6549
6676
  return next;
6550
6677
  });
6551
6678
  }, [allOnPageSelected, pageIds]);
6552
- const runOnSelection = useCallback14(
6679
+ const runOnSelection = useCallback15(
6553
6680
  async (action) => {
6554
6681
  setBusy(true);
6555
6682
  try {
@@ -6562,7 +6689,7 @@ function useConversationsInbox(params = {}) {
6562
6689
  },
6563
6690
  [selectedIds, refetch]
6564
6691
  );
6565
- const runConversationAction = useCallback14(
6692
+ const runConversationAction = useCallback15(
6566
6693
  async (action, conversationId) => {
6567
6694
  if (!action) return;
6568
6695
  setBusy(true);
@@ -6685,10 +6812,10 @@ function ConversationsWorkspace({
6685
6812
  ...markReadOnOpen === void 0 ? {} : { markReadOnOpen },
6686
6813
  ...serverPaginated ? { serverPaginated } : {}
6687
6814
  });
6688
- const [simulatorOpen, setSimulatorOpen] = useState25(false);
6689
- const [templateModalOpen, setTemplateModalOpen] = useState25(false);
6690
- const [openedFromLink, setOpenedFromLink] = useState25(void 0);
6691
- useEffect19(() => {
6815
+ const [simulatorOpen, setSimulatorOpen] = useState26(false);
6816
+ const [templateModalOpen, setTemplateModalOpen] = useState26(false);
6817
+ const [openedFromLink, setOpenedFromLink] = useState26(void 0);
6818
+ useEffect20(() => {
6692
6819
  const link = initialConversationId ?? initialWhatsappNumber;
6693
6820
  if (!link || openedFromLink === link) return;
6694
6821
  const target = inbox.conversations.find(
@@ -6708,11 +6835,11 @@ function ConversationsWorkspace({
6708
6835
  simulator && (simulator.enabled ?? true) && (simulator.render ?? transport)
6709
6836
  );
6710
6837
  const showSimulator = simulatorEnabled && simulatorOpen && Boolean(selected);
6711
- const simulatorClient = useMemo6(
6838
+ const simulatorClient = useMemo7(
6712
6839
  () => transport && selectedId ? transport({ conversationId: selectedId, channel, handle }) : void 0,
6713
6840
  [transport, selectedId, channel, handle]
6714
6841
  );
6715
- const paneUtilities = useMemo6(() => {
6842
+ const paneUtilities = useMemo7(() => {
6716
6843
  if (!selected) return void 0;
6717
6844
  const fromProduct = extraUtilitiesFor?.(selected) ?? [];
6718
6845
  if (!simulatorEnabled) return fromProduct;