@adatechnology/conversations-ui 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ import {
52
52
  StatusTicks,
53
53
  applyQuickReplyVariables,
54
54
  capabilitiesOf,
55
+ channelCapabilityFor,
55
56
  channelFiltersFor,
56
57
  contactFlag,
57
58
  conversationsOf,
@@ -76,7 +77,7 @@ import {
76
77
  useConversationLocales,
77
78
  useConversations,
78
79
  useQuickRepliesPicker
79
- } from "./chunk-L4OE4ORQ.js";
80
+ } from "./chunk-4P5KMWOI.js";
80
81
  import {
81
82
  ZERO_WIDTH_SPACE,
82
83
  cn,
@@ -1380,10 +1381,10 @@ var WINDOW_FILTERS = [
1380
1381
  var WARNING_RATIO = 0.5;
1381
1382
  var CRITICAL_RATIO = 0.875;
1382
1383
  function windowOf(params) {
1383
- const capabilities = capabilitiesOf(params.channel);
1384
- if (!capabilities.hasSessionWindow) return CONVERSATION_WINDOW.FRESH;
1384
+ const capability = channelCapabilityFor(params.channel);
1385
+ if (capability.sessionWindowHours === null) return CONVERSATION_WINDOW.FRESH;
1385
1386
  if (!params.lastInboundAt) return CONVERSATION_WINDOW.EXPIRED;
1386
- const windowMs = capabilities.windowHours * HOUR_MS;
1387
+ const windowMs = capability.sessionWindowHours * HOUR_MS;
1387
1388
  const elapsed = params.now - new Date(params.lastInboundAt).getTime();
1388
1389
  if (elapsed >= windowMs) return CONVERSATION_WINDOW.EXPIRED;
1389
1390
  if (elapsed >= windowMs * CRITICAL_RATIO) return CONVERSATION_WINDOW.CRITICAL;
@@ -1438,7 +1439,8 @@ var CHANNEL_BRAND_COLOR = {
1438
1439
  [CONVERSATION_CHANNEL.WHATSAPP]: "#25D366",
1439
1440
  [CONVERSATION_CHANNEL.MESSENGER]: "#0084FF",
1440
1441
  [CONVERSATION_CHANNEL.INSTAGRAM]: "#E4405F",
1441
- [CONVERSATION_CHANNEL.WEBCHAT]: "#64748B"
1442
+ [CONVERSATION_CHANNEL.WEBCHAT]: "#64748B",
1443
+ [CONVERSATION_CHANNEL.EMAIL]: "#EA4335"
1442
1444
  };
1443
1445
  function Glyph({ channel }) {
1444
1446
  const color = CHANNEL_BRAND_COLOR[channel];
@@ -1468,6 +1470,12 @@ function Glyph({ channel }) {
1468
1470
  /* @__PURE__ */ jsx10("circle", { cx: "16.4", cy: "7.6", r: "1", fill: "#fff" })
1469
1471
  ] });
1470
1472
  }
1473
+ if (channel === CONVERSATION_CHANNEL.EMAIL) {
1474
+ return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1475
+ /* @__PURE__ */ jsx10("rect", { x: "2", y: "4", width: "20", height: "16", rx: "3", fill: color }),
1476
+ /* @__PURE__ */ jsx10("path", { d: "M3.5 6.5 12 13l8.5-6.5", fill: "none", stroke: "#fff", strokeWidth: "1.6", strokeLinejoin: "round", strokeLinecap: "round" })
1477
+ ] });
1478
+ }
1471
1479
  return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1472
1480
  /* @__PURE__ */ jsx10("circle", { cx: "12", cy: "12", r: "11", fill: color }),
1473
1481
  /* @__PURE__ */ jsx10("path", { d: "M6.5 8.5h11v7h-5.5L8.5 18v-2.5h-2v-7Z", fill: "#fff" })
@@ -4040,7 +4048,30 @@ function MessagesWorkspace({
4040
4048
  }
4041
4049
 
4042
4050
  // src/hooks/useConversationMessages.ts
4043
- import { useCallback as useCallback7 } from "react";
4051
+ import { useCallback as useCallback7, useEffect as useEffect10, useMemo as useMemo3, useRef as useRef7, useState as useState15 } from "react";
4052
+ var OLDER_MESSAGES_PAGE_LIMIT = 50;
4053
+ function dedupeMessagesById(messages) {
4054
+ const seenIds = /* @__PURE__ */ new Set();
4055
+ const deduped = [];
4056
+ for (const message of messages) {
4057
+ if (seenIds.has(message.id)) continue;
4058
+ seenIds.add(message.id);
4059
+ deduped.push(message);
4060
+ }
4061
+ return deduped;
4062
+ }
4063
+ function prependOlderMessages(accumulated, olderPage) {
4064
+ const existingIds = new Set(accumulated.map((message) => message.id));
4065
+ const newMessages = olderPage.filter((message) => !existingIds.has(message.id));
4066
+ return [...newMessages, ...accumulated];
4067
+ }
4068
+ function shouldFetchOlderMessages(state) {
4069
+ if (state.isLoading || !state.hasMore) return false;
4070
+ return Boolean(state.oldestMessage);
4071
+ }
4072
+ function pageIndicatesMoreOlderMessages(pageLength, limit) {
4073
+ return pageLength >= limit;
4074
+ }
4044
4075
  function useConversationMessages(conversationId, params) {
4045
4076
  const context = useConversations();
4046
4077
  if (!context) {
@@ -4051,6 +4082,38 @@ function useConversationMessages(conversationId, params) {
4051
4082
  () => api.fetchMessages(conversationId, params),
4052
4083
  [conversationId, params?.limit, params?.before]
4053
4084
  );
4085
+ const [olderMessages, setOlderMessages] = useState15([]);
4086
+ const [hasMoreOlderMessages, setHasMoreOlderMessages] = useState15(true);
4087
+ const [loadingOlderMessages, setLoadingOlderMessages] = useState15(false);
4088
+ const loadingOlderMessagesRef = useRef7(false);
4089
+ useEffect10(() => {
4090
+ setOlderMessages([]);
4091
+ setHasMoreOlderMessages(true);
4092
+ }, [conversationId]);
4093
+ const messages = useMemo3(() => dedupeMessagesById([...olderMessages, ...data ?? []]), [olderMessages, data]);
4094
+ const loadOlderMessages = useCallback7(async () => {
4095
+ const oldestMessage = messages[0];
4096
+ if (!shouldFetchOlderMessages({
4097
+ isLoading: loadingOlderMessagesRef.current,
4098
+ hasMore: hasMoreOlderMessages,
4099
+ oldestMessage
4100
+ }))
4101
+ return;
4102
+ if (!oldestMessage) return;
4103
+ loadingOlderMessagesRef.current = true;
4104
+ setLoadingOlderMessages(true);
4105
+ try {
4106
+ const olderPage = await api.fetchMessages(conversationId, {
4107
+ limit: OLDER_MESSAGES_PAGE_LIMIT,
4108
+ before: oldestMessage.timestamp
4109
+ });
4110
+ if (!pageIndicatesMoreOlderMessages(olderPage.length, OLDER_MESSAGES_PAGE_LIMIT)) setHasMoreOlderMessages(false);
4111
+ setOlderMessages((current) => prependOlderMessages(current, olderPage));
4112
+ } finally {
4113
+ loadingOlderMessagesRef.current = false;
4114
+ setLoadingOlderMessages(false);
4115
+ }
4116
+ }, [api, conversationId, hasMoreOlderMessages, messages]);
4054
4117
  const sendMessage = useCallback7(
4055
4118
  async (text) => {
4056
4119
  const message = await api.sendMessage(conversationId, text);
@@ -4075,20 +4138,32 @@ function useConversationMessages(conversationId, params) {
4075
4138
  [api, conversationId, refetch]
4076
4139
  );
4077
4140
  const markRead = useCallback7(() => api.markRead(conversationId), [api, conversationId]);
4078
- return { messages: data ?? [], loading, error, refetch, sendMessage, sendMedia, sendTemplate, markRead };
4141
+ return {
4142
+ messages,
4143
+ loading,
4144
+ error,
4145
+ refetch,
4146
+ sendMessage,
4147
+ sendMedia,
4148
+ sendTemplate,
4149
+ markRead,
4150
+ loadOlderMessages,
4151
+ loadingOlderMessages,
4152
+ hasMoreOlderMessages
4153
+ };
4079
4154
  }
4080
4155
 
4081
4156
  // src/hooks/useScrollToLatestMessage.ts
4082
- import { useCallback as useCallback8, useLayoutEffect, useRef as useRef7, useState as useState15 } from "react";
4157
+ import { useCallback as useCallback8, useLayoutEffect, useRef as useRef8, useState as useState16 } from "react";
4083
4158
  var NEAR_BOTTOM_THRESHOLD_PX = 120;
4084
4159
  var SCROLL_BEHAVIOR = "auto";
4085
4160
  function useScrollToLatestMessage({
4086
4161
  conversationId,
4087
4162
  messageCount
4088
4163
  }) {
4089
- const containerRef = useRef7(null);
4090
- const [isAwayFromBottom, setIsAwayFromBottom] = useState15(false);
4091
- const isAwayFromBottomRef = useRef7(false);
4164
+ const containerRef = useRef8(null);
4165
+ const [isAwayFromBottom, setIsAwayFromBottom] = useState16(false);
4166
+ const isAwayFromBottomRef = useRef8(false);
4092
4167
  const scrollToBottom = useCallback8((behavior = SCROLL_BEHAVIOR) => {
4093
4168
  const container = containerRef.current;
4094
4169
  if (!container) return;
@@ -4101,7 +4176,7 @@ function useScrollToLatestMessage({
4101
4176
  isAwayFromBottomRef.current = away;
4102
4177
  setIsAwayFromBottom((current) => current === away ? current : away);
4103
4178
  }, []);
4104
- const jumpedForConversationRef = useRef7(void 0);
4179
+ const jumpedForConversationRef = useRef8(void 0);
4105
4180
  useLayoutEffect(() => {
4106
4181
  const isNewConversation = jumpedForConversationRef.current !== conversationId;
4107
4182
  if (isNewConversation) {
@@ -4148,12 +4223,12 @@ function useConversationContext(conversationId) {
4148
4223
  }
4149
4224
 
4150
4225
  // src/hooks/useConversationRealtime.ts
4151
- import { useEffect as useEffect10, useRef as useRef8 } from "react";
4226
+ import { useEffect as useEffect11, useRef as useRef9 } from "react";
4152
4227
  function useConversationRealtime(conversationId, onEvent) {
4153
4228
  const sse = useConversations()?.sse;
4154
- const onEventRef = useRef8(onEvent);
4229
+ const onEventRef = useRef9(onEvent);
4155
4230
  onEventRef.current = onEvent;
4156
- useEffect10(() => {
4231
+ useEffect11(() => {
4157
4232
  if (!sse || !conversationId) return;
4158
4233
  const source = sse.connectConversationStream(conversationId);
4159
4234
  const handler = (event) => onEventRef.current(event);
@@ -4166,9 +4241,9 @@ function useConversationRealtime(conversationId, onEvent) {
4166
4241
  }
4167
4242
  function useGlobalRealtime(onEvent) {
4168
4243
  const sse = useConversations()?.sse;
4169
- const onEventRef = useRef8(onEvent);
4244
+ const onEventRef = useRef9(onEvent);
4170
4245
  onEventRef.current = onEvent;
4171
- useEffect10(() => {
4246
+ useEffect11(() => {
4172
4247
  if (!sse) return;
4173
4248
  const source = sse.connectGlobalStream();
4174
4249
  const handler = (event) => onEventRef.current(event);
@@ -4181,14 +4256,14 @@ function useGlobalRealtime(onEvent) {
4181
4256
  }
4182
4257
 
4183
4258
  // src/hooks/useConversationActions.ts
4184
- import { useMemo as useMemo3 } from "react";
4259
+ import { useMemo as useMemo4 } from "react";
4185
4260
  function useConversationActions(conversationId) {
4186
4261
  const context = useConversations();
4187
4262
  if (!context) {
4188
4263
  throw new Error("useConversationActions requires an ancestor <ConversationsProvider>");
4189
4264
  }
4190
4265
  const { api } = context;
4191
- return useMemo3(
4266
+ return useMemo4(
4192
4267
  () => ({
4193
4268
  takeover: api.takeover ? () => api.takeover(conversationId) : void 0,
4194
4269
  release: api.release ? () => api.release(conversationId) : void 0,
@@ -4203,7 +4278,7 @@ function useInboxActions() {
4203
4278
  throw new Error("useInboxActions requires an ancestor <ConversationsProvider>");
4204
4279
  }
4205
4280
  const { api } = context;
4206
- return useMemo3(
4281
+ return useMemo4(
4207
4282
  () => ({
4208
4283
  markAllRead: api.markAllRead ? () => api.markAllRead() : void 0,
4209
4284
  listTemplates: api.listTemplates ? () => api.listTemplates() : void 0
@@ -4347,11 +4422,11 @@ async function sendQueuedMessage(params) {
4347
4422
  }
4348
4423
 
4349
4424
  // src/quickReplies/QuickRepliesWorkspace.tsx
4350
- import { useEffect as useEffect13, useId as useId2, useRef as useRef11, useState as useState18 } from "react";
4425
+ import { useEffect as useEffect14, useId as useId2, useRef as useRef12, useState as useState19 } from "react";
4351
4426
  import { Pencil as Pencil2, Plus as Plus2, Trash2 as Trash23 } from "lucide-react";
4352
4427
 
4353
4428
  // src/quickReplies/useQuickRepliesWorkspace.ts
4354
- import { useCallback as useCallback10, useEffect as useEffect12, useState as useState17 } from "react";
4429
+ import { useCallback as useCallback10, useEffect as useEffect13, useState as useState18 } from "react";
4355
4430
 
4356
4431
  // src/quickReplies/quickReplyAttachmentUpload.ts
4357
4432
  function validateAttachmentFiles(files, currentCount, limits) {
@@ -4383,7 +4458,7 @@ function moveAttachment(list, index, direction) {
4383
4458
  }
4384
4459
 
4385
4460
  // src/quickReplies/useQuickReplyAttachmentUploads.ts
4386
- import { useCallback as useCallback9, useEffect as useEffect11, useRef as useRef9, useState as useState16 } from "react";
4461
+ import { useCallback as useCallback9, useEffect as useEffect12, useRef as useRef10, useState as useState17 } from "react";
4387
4462
 
4388
4463
  // src/quickReplies/createUploadQueue.ts
4389
4464
  function createUploadQueue(maxConcurrent) {
@@ -4439,11 +4514,11 @@ function useQuickReplyAttachmentUploads({
4439
4514
  attachmentsCount,
4440
4515
  onUploaded
4441
4516
  }) {
4442
- const [pendingUploads, setPendingUploads] = useState16([]);
4443
- const [attachmentRejections, setAttachmentRejections] = useState16([]);
4444
- const uploadQueueRef = useRef9(createUploadQueue(3));
4445
- const isMountedRef = useRef9(true);
4446
- useEffect11(
4517
+ const [pendingUploads, setPendingUploads] = useState17([]);
4518
+ const [attachmentRejections, setAttachmentRejections] = useState17([]);
4519
+ const uploadQueueRef = useRef10(createUploadQueue(3));
4520
+ const isMountedRef = useRef10(true);
4521
+ useEffect12(
4447
4522
  () => () => {
4448
4523
  isMountedRef.current = false;
4449
4524
  uploadQueueRef.current.abortAll();
@@ -4621,15 +4696,15 @@ function useQuickRepliesWorkspace({
4621
4696
  labels,
4622
4697
  attachmentSizeLimits
4623
4698
  }) {
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);
4699
+ const [quickReplies, setQuickReplies] = useState18([]);
4700
+ const [isLoading, setIsLoading] = useState18(false);
4701
+ const [loadError, setLoadError] = useState18(void 0);
4702
+ const [search, setSearch] = useState18("");
4703
+ const [editing, setEditing] = useState18(void 0);
4704
+ const [fieldErrors, setFieldErrors] = useState18({});
4705
+ const [isSaving, setIsSaving] = useState18(false);
4706
+ const [saveError, setSaveError] = useState18(void 0);
4707
+ const [deletingId, setDeletingId] = useState18(void 0);
4633
4708
  const uploadAttachment = useCallback10((attachment) => {
4634
4709
  setEditing((current) => current ? { ...current, attachments: [...current.attachments, attachment] } : current);
4635
4710
  }, []);
@@ -4648,7 +4723,7 @@ function useQuickRepliesWorkspace({
4648
4723
  attachmentsCount: editing?.attachments.length ?? 0,
4649
4724
  onUploaded: uploadAttachment
4650
4725
  });
4651
- useEffect12(() => {
4726
+ useEffect13(() => {
4652
4727
  if (!api.listQuickReplies) return;
4653
4728
  let cancelled = false;
4654
4729
  setIsLoading(true);
@@ -4976,7 +5051,7 @@ function QuickReplyWhatsAppPreview({ body, variables, attachments, labels }) {
4976
5051
  }
4977
5052
 
4978
5053
  // src/quickReplies/AttachmentsFormSection.tsx
4979
- import { useRef as useRef10 } from "react";
5054
+ import { useRef as useRef11 } from "react";
4980
5055
  import { Paperclip as Paperclip3, X as X4 } from "lucide-react";
4981
5056
  import { Fragment as Fragment7, jsx as jsx27, jsxs as jsxs24 } from "react/jsx-runtime";
4982
5057
  function AttachmentsFormSection({
@@ -4991,7 +5066,7 @@ function AttachmentsFormSection({
4991
5066
  onMoveAttachment,
4992
5067
  onDismissRejections
4993
5068
  }) {
4994
- const attachmentFileInputRef = useRef10(null);
5069
+ const attachmentFileInputRef = useRef11(null);
4995
5070
  return /* @__PURE__ */ jsxs24("div", { className: "space-y-2", children: [
4996
5071
  /* @__PURE__ */ jsx27("span", { className: "block text-sm font-medium", children: text.attachmentsTitle }),
4997
5072
  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 +5201,15 @@ function QuickRepliesWorkspace({
5126
5201
  attachmentSizeLimits
5127
5202
  }) {
5128
5203
  const text = { ...DEFAULT_QUICK_REPLIES_WORKSPACE_LABELS, ...labels };
5129
- const bodyFieldRef = useRef11(null);
5204
+ const bodyFieldRef = useRef12(null);
5130
5205
  const formId = useId2();
5131
5206
  const titleFieldId = `${formId}-title`;
5132
5207
  const shortcutFieldId = `${formId}-shortcut`;
5133
5208
  const bodyFieldId = `${formId}-body`;
5134
- const [confirmingDeleteId, setConfirmingDeleteId] = useState18(null);
5135
- const deleteButtonRefs = useRef11({});
5136
- const confirmButtonRef = useRef11(null);
5137
- useEffect13(() => {
5209
+ const [confirmingDeleteId, setConfirmingDeleteId] = useState19(null);
5210
+ const deleteButtonRefs = useRef12({});
5211
+ const confirmButtonRef = useRef12(null);
5212
+ useEffect14(() => {
5138
5213
  if (confirmingDeleteId) confirmButtonRef.current?.focus();
5139
5214
  }, [confirmingDeleteId]);
5140
5215
  const cancelDeleteConfirm = (id) => {
@@ -5469,11 +5544,11 @@ function QuickRepliesWorkspace({
5469
5544
  }
5470
5545
 
5471
5546
  // src/workspace/ConversationsWorkspace.tsx
5472
- import { useEffect as useEffect19, useMemo as useMemo6, useState as useState25 } from "react";
5547
+ import { useEffect as useEffect20, useMemo as useMemo7, useState as useState26 } from "react";
5473
5548
  import { Check as Check2, CheckCheck, FlaskConical, Hourglass as Hourglass2, Mail as Mail2, MessagesSquare } from "lucide-react";
5474
5549
 
5475
5550
  // src/workspace/BulkTemplateModal.tsx
5476
- import { useEffect as useEffect14, useMemo as useMemo4, useState as useState19 } from "react";
5551
+ import { useEffect as useEffect15, useMemo as useMemo5, useState as useState20 } from "react";
5477
5552
  import { jsx as jsx29, jsxs as jsxs26 } from "react/jsx-runtime";
5478
5553
  function BulkTemplateModal({
5479
5554
  labels,
@@ -5487,10 +5562,10 @@ function BulkTemplateModal({
5487
5562
  throw new Error("BulkTemplateModal requires an ancestor <ConversationsProvider>");
5488
5563
  }
5489
5564
  const { api } = context;
5490
- const [templates, setTemplates] = useState19([]);
5491
- const [search, setSearch] = useState19("");
5492
- const [selected, setSelected] = useState19("");
5493
- useEffect14(() => {
5565
+ const [templates, setTemplates] = useState20([]);
5566
+ const [search, setSearch] = useState20("");
5567
+ const [selected, setSelected] = useState20("");
5568
+ useEffect15(() => {
5494
5569
  let active = true;
5495
5570
  void api.listTemplates?.().then((loaded) => {
5496
5571
  if (active) setTemplates(loaded);
@@ -5500,7 +5575,7 @@ function BulkTemplateModal({
5500
5575
  active = false;
5501
5576
  };
5502
5577
  }, [api]);
5503
- const filtered = useMemo4(() => {
5578
+ const filtered = useMemo5(() => {
5504
5579
  const term = search.trim().toLowerCase();
5505
5580
  if (!term) return templates;
5506
5581
  return templates.filter((template) => template.name.toLowerCase().includes(term));
@@ -5564,7 +5639,48 @@ function BulkTemplateModal({
5564
5639
  }
5565
5640
 
5566
5641
  // src/workspace/ConversationPane.tsx
5567
- import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef15, useState as useState23 } from "react";
5642
+ import { useCallback as useCallback14, useEffect as useEffect18, useRef as useRef17, useState as useState24 } from "react";
5643
+
5644
+ // src/hooks/useLoadOlderMessagesScroll.ts
5645
+ import { useCallback as useCallback11, useLayoutEffect as useLayoutEffect2, useRef as useRef13 } from "react";
5646
+ var NEAR_TOP_THRESHOLD_PX = 200;
5647
+ function computeScrollTopAfterPrepend(before, scrollHeightAfter) {
5648
+ return before.scrollTop + (scrollHeightAfter - before.scrollHeight);
5649
+ }
5650
+ function useLoadOlderMessagesScroll({
5651
+ containerRef,
5652
+ oldestMessageId,
5653
+ hasMore,
5654
+ loading,
5655
+ onLoadOlder
5656
+ }) {
5657
+ const pendingSnapshotRef = useRef13(null);
5658
+ const previousOldestMessageIdRef = useRef13(oldestMessageId);
5659
+ const triggerLoadOlder = useCallback11(() => {
5660
+ if (loading || !hasMore) return;
5661
+ const container = containerRef.current;
5662
+ if (!container) return;
5663
+ pendingSnapshotRef.current = { scrollHeight: container.scrollHeight, scrollTop: container.scrollTop };
5664
+ onLoadOlder();
5665
+ }, [containerRef, hasMore, loading, onLoadOlder]);
5666
+ const handleScroll = useCallback11(
5667
+ (event) => {
5668
+ if (event.currentTarget.scrollTop > NEAR_TOP_THRESHOLD_PX) return;
5669
+ triggerLoadOlder();
5670
+ },
5671
+ [triggerLoadOlder]
5672
+ );
5673
+ useLayoutEffect2(() => {
5674
+ const idChanged = previousOldestMessageIdRef.current !== oldestMessageId;
5675
+ previousOldestMessageIdRef.current = oldestMessageId;
5676
+ const container = containerRef.current;
5677
+ const pending = pendingSnapshotRef.current;
5678
+ if (!idChanged || !container || !pending) return;
5679
+ pendingSnapshotRef.current = null;
5680
+ container.scrollTop = computeScrollTopAfterPrepend(pending, container.scrollHeight);
5681
+ }, [oldestMessageId, containerRef]);
5682
+ return { handleScroll, triggerLoadOlder };
5683
+ }
5568
5684
 
5569
5685
  // src/quickReplies/resolveConversationVariables.ts
5570
5686
  var MARKER_PATTERN = /^\{\{\s*([\w.]+)\s*\}\}$/;
@@ -5585,7 +5701,7 @@ function resolveConversationVariables({
5585
5701
  }
5586
5702
 
5587
5703
  // src/workspace/QueuedAttachmentsList.tsx
5588
- import { useEffect as useEffect15, useRef as useRef12, useState as useState20 } from "react";
5704
+ import { useEffect as useEffect16, useRef as useRef14, useState as useState21 } from "react";
5589
5705
  import { jsx as jsx30, jsxs as jsxs27 } from "react/jsx-runtime";
5590
5706
  function nameOf(item) {
5591
5707
  return item.kind === "local" ? item.file.name : item.filename;
@@ -5601,16 +5717,16 @@ function AttachmentThumbnail({
5601
5717
  getThumbnailUrl
5602
5718
  }) {
5603
5719
  const isImage = mimeTypeOf(item).startsWith("image/");
5604
- const [url, setUrl] = useState20(item.kind === "local" ? void 0 : item.previewUrl);
5720
+ const [url, setUrl] = useState21(item.kind === "local" ? void 0 : item.previewUrl);
5605
5721
  const localFile = item.kind === "local" ? item.file : void 0;
5606
5722
  const uploadId = item.kind === "stored" ? item.uploadId : void 0;
5607
- useEffect15(() => {
5723
+ useEffect16(() => {
5608
5724
  if (!isImage || !localFile) return;
5609
5725
  const objectUrl = URL.createObjectURL(localFile);
5610
5726
  setUrl(objectUrl);
5611
5727
  return () => URL.revokeObjectURL(objectUrl);
5612
5728
  }, [isImage, localFile]);
5613
- useEffect15(() => {
5729
+ useEffect16(() => {
5614
5730
  if (!isImage || !uploadId || !getThumbnailUrl) return;
5615
5731
  let cancelled = false;
5616
5732
  getThumbnailUrl(uploadId).then((resolved) => {
@@ -5628,11 +5744,11 @@ function departedItemsOf(previouslyRendered, items) {
5628
5744
  return previouslyRendered.filter((item) => !currentKeys.has(attachmentKey(item)));
5629
5745
  }
5630
5746
  function useDepartingItems(items) {
5631
- const [rendered, setRendered] = useState20(items);
5632
- const [departingKeys, setDepartingKeys] = useState20(/* @__PURE__ */ new Set());
5633
- const renderedRef = useRef12(rendered);
5747
+ const [rendered, setRendered] = useState21(items);
5748
+ const [departingKeys, setDepartingKeys] = useState21(/* @__PURE__ */ new Set());
5749
+ const renderedRef = useRef14(rendered);
5634
5750
  renderedRef.current = rendered;
5635
- useEffect15(() => {
5751
+ useEffect16(() => {
5636
5752
  const removedItems = departedItemsOf(renderedRef.current, items);
5637
5753
  if (removedItems.length === 0) {
5638
5754
  setRendered(items);
@@ -5716,10 +5832,10 @@ function QueuedAttachmentsList({
5716
5832
  }
5717
5833
 
5718
5834
  // src/workspace/useComposerQueue.ts
5719
- import { useCallback as useCallback12, useEffect as useEffect16, useRef as useRef14, useState as useState22 } from "react";
5835
+ import { useCallback as useCallback13, useEffect as useEffect17, useRef as useRef16, useState as useState23 } from "react";
5720
5836
 
5721
5837
  // src/workspace/useComposerAttachmentRetry.ts
5722
- import { useCallback as useCallback11, useRef as useRef13, useState as useState21 } from "react";
5838
+ import { useCallback as useCallback12, useRef as useRef15, useState as useState22 } from "react";
5723
5839
  function useComposerAttachmentRetry(params) {
5724
5840
  const {
5725
5841
  conversationId,
@@ -5733,9 +5849,9 @@ function useComposerAttachmentRetry(params) {
5733
5849
  labels,
5734
5850
  sendInFlightRef
5735
5851
  } = params;
5736
- const retryIdempotencyKeyRef = useRef13(void 0);
5737
- const retryingKeysRef = useRef13(/* @__PURE__ */ new Set());
5738
- const [retryingKeys, setRetryingKeys] = useState21(/* @__PURE__ */ new Set());
5852
+ const retryIdempotencyKeyRef = useRef15(void 0);
5853
+ const retryingKeysRef = useRef15(/* @__PURE__ */ new Set());
5854
+ const [retryingKeys, setRetryingKeys] = useState22(/* @__PURE__ */ new Set());
5739
5855
  function markRetrying(key) {
5740
5856
  retryingKeysRef.current.add(key);
5741
5857
  setRetryingKeys(new Set(retryingKeysRef.current));
@@ -5811,7 +5927,7 @@ function useComposerAttachmentRetry(params) {
5811
5927
  }
5812
5928
  void retryStoredAttachment(item).finally(release);
5813
5929
  }
5814
- const resetRetryState = useCallback11(() => {
5930
+ const resetRetryState = useCallback12(() => {
5815
5931
  retryIdempotencyKeyRef.current = void 0;
5816
5932
  retryingKeysRef.current.clear();
5817
5933
  setRetryingKeys(/* @__PURE__ */ new Set());
@@ -5822,12 +5938,12 @@ function useComposerAttachmentRetry(params) {
5822
5938
  // src/workspace/useComposerQueue.ts
5823
5939
  function useComposerQueue(params) {
5824
5940
  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);
5941
+ const [queue, setQueue] = useState23([]);
5942
+ const [attachmentStatus, setAttachmentStatus] = useState23({});
5943
+ const [isSendingDraft, setIsSendingDraft] = useState23(false);
5944
+ const sendInFlightRef = useRef16(false);
5945
+ const idempotencyKeyRef = useRef16(void 0);
5946
+ const currentConversationIdRef = useRef16(conversationId);
5831
5947
  const { retryQueuedAttachment, resetRetryState, retryingKeys } = useComposerAttachmentRetry({
5832
5948
  conversationId,
5833
5949
  currentConversationIdRef,
@@ -5840,7 +5956,7 @@ function useComposerQueue(params) {
5840
5956
  sendInFlightRef,
5841
5957
  ...onSendAttachments ? { onSendAttachments } : {}
5842
5958
  });
5843
- useEffect16(() => {
5959
+ useEffect17(() => {
5844
5960
  currentConversationIdRef.current = conversationId;
5845
5961
  setQueue([]);
5846
5962
  setAttachmentStatus({});
@@ -5849,11 +5965,11 @@ function useComposerQueue(params) {
5849
5965
  setIsSendingDraft(false);
5850
5966
  resetRetryState();
5851
5967
  }, [conversationId, resetRetryState]);
5852
- const enqueueAttachments = useCallback12((items) => {
5968
+ const enqueueAttachments = useCallback13((items) => {
5853
5969
  if (items.length === 0) return;
5854
5970
  setQueue((current) => [...current, ...items]);
5855
5971
  }, []);
5856
- const removeQueuedAttachment = useCallback12((item) => {
5972
+ const removeQueuedAttachment = useCallback13((item) => {
5857
5973
  setQueue((current) => current.filter((queued) => attachmentKey(queued) !== attachmentKey(item)));
5858
5974
  }, []);
5859
5975
  async function runSend(action, fallback) {
@@ -5991,12 +6107,14 @@ function ConversationPane({
5991
6107
  throw new Error("ConversationPane requires an ancestor <ConversationsProvider>");
5992
6108
  }
5993
6109
  const { api } = context;
5994
- const { messages, refetch } = useConversationMessages(conversation.id);
6110
+ const { messages, refetch, loadOlderMessages, loadingOlderMessages, hasMoreOlderMessages } = useConversationMessages(
6111
+ conversation.id
6112
+ );
5995
6113
  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 ?? "");
6114
+ const [documentsOpen, setDocumentsOpen] = useState24(false);
6115
+ const [sendFailure, setSendFailure] = useState24(void 0);
6116
+ const [selectedMessageIds, setSelectedMessageIds] = useState24(/* @__PURE__ */ new Set());
6117
+ const [draft, setDraft] = useState24(initialComposerText ?? "");
6000
6118
  const {
6001
6119
  queue,
6002
6120
  enqueueAttachments,
@@ -6016,8 +6134,8 @@ function ConversationPane({
6016
6134
  refetch,
6017
6135
  setSendFailure
6018
6136
  });
6019
- const thumbnailUrlCacheRef = useRef15(/* @__PURE__ */ new Map());
6020
- const getQueuedAttachmentThumbnailUrl = useCallback13(
6137
+ const thumbnailUrlCacheRef = useRef17(/* @__PURE__ */ new Map());
6138
+ const getQueuedAttachmentThumbnailUrl = useCallback14(
6021
6139
  (uploadId) => {
6022
6140
  const cached = thumbnailUrlCacheRef.current.get(uploadId);
6023
6141
  if (cached) return cached;
@@ -6028,7 +6146,7 @@ function ConversationPane({
6028
6146
  },
6029
6147
  [api]
6030
6148
  );
6031
- useEffect17(() => {
6149
+ useEffect18(() => {
6032
6150
  setSelectedMessageIds(/* @__PURE__ */ new Set());
6033
6151
  setDraft(initialComposerText ?? "");
6034
6152
  }, [conversation.id, initialComposerText]);
@@ -6048,6 +6166,13 @@ function ConversationPane({
6048
6166
  setSelectedMessageIds(/* @__PURE__ */ new Set());
6049
6167
  }
6050
6168
  const scroll = useScrollToLatestMessage({ conversationId: conversation.id, messageCount: messages.length });
6169
+ const loadOlder = useLoadOlderMessagesScroll({
6170
+ containerRef: scroll.containerRef,
6171
+ oldestMessageId: messages[0]?.id,
6172
+ hasMore: hasMoreOlderMessages,
6173
+ loading: loadingOlderMessages,
6174
+ onLoadOlder: () => void loadOlderMessages()
6175
+ });
6051
6176
  useConversationRealtime(conversation.id, () => {
6052
6177
  void refetch();
6053
6178
  });
@@ -6131,32 +6256,39 @@ function ConversationPane({
6131
6256
  contextEntries && contextEntries.length > 0 || flowLabel ? /* @__PURE__ */ jsx31(ConversationContextPanel, { entries: contextEntries ?? [], ...flowLabel ? { flowLabel } : {} }) : null,
6132
6257
  /* @__PURE__ */ jsx31(ConversationDocumentsPanel, { conversationId: conversation.id, open: documentsOpen }),
6133
6258
  renderAboveTranscript?.(conversation, conversationContext),
6134
- /* @__PURE__ */ jsx31(
6259
+ /* @__PURE__ */ jsxs28(
6135
6260
  ConversationWallpaper,
6136
6261
  {
6137
6262
  ref: scroll.containerRef,
6138
- onScroll: scroll.handleScroll,
6263
+ onScroll: (event) => {
6264
+ scroll.handleScroll(event);
6265
+ loadOlder.handleScroll(event);
6266
+ },
6139
6267
  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
- })
6268
+ children: [
6269
+ 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,
6270
+ messages.map((message, index) => {
6271
+ const previous = index > 0 ? messages[index - 1] : void 0;
6272
+ const startsNewDay = !previous || new Date(message.timestamp).toDateString() !== new Date(previous.timestamp).toDateString();
6273
+ return /* @__PURE__ */ jsxs28("div", { children: [
6274
+ startsNewDay ? /* @__PURE__ */ jsx31(DateDivider, { iso: message.timestamp }) : null,
6275
+ /* @__PURE__ */ jsx31(
6276
+ MessageBubble,
6277
+ {
6278
+ message,
6279
+ isMine: message.direction === "outbound",
6280
+ channel: conversation.channel,
6281
+ isFirstInGroup: !previous || previous.sender !== message.sender,
6282
+ ...messageSelection ? {
6283
+ isSelecting: selectedMessageIds.size > 0,
6284
+ isSelected: selectedMessageIds.has(message.id),
6285
+ onToggleSelect: () => toggleMessageSelected(message.id)
6286
+ } : {}
6287
+ }
6288
+ )
6289
+ ] }, message.id);
6290
+ })
6291
+ ]
6160
6292
  }
6161
6293
  ),
6162
6294
  messageSelection && selectedMessageIds.size > 0 ? /* @__PURE__ */ jsxs28("div", { className: "cv-workspace-selection", children: [
@@ -6196,9 +6328,11 @@ function ConversationPane({
6196
6328
  onSend: () => void handleRichSend(),
6197
6329
  ...onSendAttachments ? {
6198
6330
  onAttachFiles: (files) => enqueueAttachments(
6199
- Array.from(files).map(
6200
- (file) => ({ kind: "local", localId: crypto.randomUUID(), file })
6201
- )
6331
+ Array.from(files).map((file) => ({
6332
+ kind: "local",
6333
+ localId: crypto.randomUUID(),
6334
+ file
6335
+ }))
6202
6336
  )
6203
6337
  } : onAttach ? {
6204
6338
  onAttachFiles: (files) => {
@@ -6250,6 +6384,7 @@ function ConversationPane({
6250
6384
  value: draft,
6251
6385
  onChange: setDraft,
6252
6386
  onSend: (text) => void handleSend(text),
6387
+ channel: conversation.channel,
6253
6388
  ...onAttach ? { onAttach: (file) => void handleAttach(file) } : {},
6254
6389
  placeholder: labels.composerPlaceholder,
6255
6390
  ...quickReplies ? { quickReplies } : {},
@@ -6444,6 +6579,8 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
6444
6579
  templateModalSend: (count) => `Enviar para ${count}`,
6445
6580
  messagesSelected: (count) => `${count} mensagem${count === 1 ? "" : "s"} selecionada${count === 1 ? "" : "s"}`,
6446
6581
  copySelected: "Copiar",
6582
+ loadOlderMessages: "Carregar mensagens anteriores",
6583
+ loadingOlderMessages: "Carregando\u2026",
6447
6584
  composerPlaceholder: "Responder como atendente\u2026",
6448
6585
  attachFailure: "Falha ao enviar o arquivo.",
6449
6586
  attachmentRemove: "Remover anexo",
@@ -6462,7 +6599,7 @@ var DEFAULT_CONVERSATIONS_WORKSPACE_LABELS = {
6462
6599
  };
6463
6600
 
6464
6601
  // src/workspace/useConversationsInbox.ts
6465
- import { useCallback as useCallback14, useEffect as useEffect18, useMemo as useMemo5, useRef as useRef16, useState as useState24 } from "react";
6602
+ import { useCallback as useCallback15, useEffect as useEffect19, useMemo as useMemo6, useRef as useRef18, useState as useState25 } from "react";
6466
6603
  var CONVERSATIONS_PER_PAGE = 50;
6467
6604
  function defaultDescribeFailure(error) {
6468
6605
  const status = error.status;
@@ -6480,14 +6617,14 @@ function useConversationsInbox(params = {}) {
6480
6617
  const perPage = params.perPage ?? CONVERSATIONS_PER_PAGE;
6481
6618
  const markReadOnOpen = params.markReadOnOpen ?? true;
6482
6619
  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);
6620
+ const [waitingOnly, setWaitingOnly] = useState25(false);
6621
+ const [windowFilter, setWindowFilter] = useState25(CONVERSATION_WINDOW.ALL);
6622
+ const [channelFilter, setChannelFilter] = useState25(CHANNEL_FILTER_ALL);
6623
+ const [search, setSearch] = useState25("");
6624
+ const [page, setPage] = useState25(1);
6625
+ const [selectedId, setSelectedId] = useState25(void 0);
6626
+ const [selectedIds, setSelectedIds] = useState25(/* @__PURE__ */ new Set());
6627
+ const [busy, setBusy] = useState25(false);
6491
6628
  const { conversations, total, loading, error, refetch } = useConversationList({
6492
6629
  ...waitingOnly ? { waitingHuman: true } : {},
6493
6630
  ...search ? { search } : {},
@@ -6495,12 +6632,12 @@ function useConversationsInbox(params = {}) {
6495
6632
  ...params.serverPaginated ? { page, limit: perPage } : {}
6496
6633
  });
6497
6634
  useGlobalRealtime(
6498
- useCallback14(() => {
6635
+ useCallback15(() => {
6499
6636
  void refetch();
6500
6637
  }, [refetch])
6501
6638
  );
6502
- const now = useMemo5(() => Date.now(), [conversations]);
6503
- const filtered = useMemo5(
6639
+ const now = useMemo6(() => Date.now(), [conversations]);
6640
+ const filtered = useMemo6(
6504
6641
  () => conversations.filter(
6505
6642
  (conversation) => channelFilter === CHANNEL_FILTER_ALL || (conversation.channel ?? DEFAULT_CONVERSATION_CHANNEL) === channelFilter
6506
6643
  ).filter(
@@ -6512,16 +6649,16 @@ function useConversationsInbox(params = {}) {
6512
6649
  const pageCount = Math.max(1, Math.ceil(totalForPaging / perPage));
6513
6650
  const currentPage = Math.min(page, pageCount);
6514
6651
  const pageConversations = params.serverPaginated ? filtered : filtered.slice((currentPage - 1) * perPage, currentPage * perPage);
6515
- useEffect18(() => {
6652
+ useEffect19(() => {
6516
6653
  setPage(1);
6517
6654
  }, [search, waitingOnly, windowFilter, channelFilter]);
6518
- useEffect18(() => {
6655
+ useEffect19(() => {
6519
6656
  if (selectedId && !conversations.some((conversation) => conversation.id === selectedId)) {
6520
6657
  setSelectedId(void 0);
6521
6658
  }
6522
6659
  }, [conversations, selectedId]);
6523
- const lastMarkReadAttempt = useRef16(void 0);
6524
- useEffect18(() => {
6660
+ const lastMarkReadAttempt = useRef18(void 0);
6661
+ useEffect19(() => {
6525
6662
  if (!markReadOnOpen || !selectedId) return;
6526
6663
  const opened = conversations.find((conversation) => conversation.id === selectedId);
6527
6664
  if (!opened || opened.unread === 0) return;
@@ -6531,7 +6668,7 @@ function useConversationsInbox(params = {}) {
6531
6668
  void api.markRead(selectedId).then(() => refetch()).catch(() => {
6532
6669
  });
6533
6670
  }, [markReadOnOpen, selectedId, conversations, refetch, api]);
6534
- const toggleSelected = useCallback14((conversationId) => {
6671
+ const toggleSelected = useCallback15((conversationId) => {
6535
6672
  setSelectedIds((current) => {
6536
6673
  const next = new Set(current);
6537
6674
  if (next.has(conversationId)) next.delete(conversationId);
@@ -6541,7 +6678,7 @@ function useConversationsInbox(params = {}) {
6541
6678
  }, []);
6542
6679
  const pageIds = pageConversations.map((conversation) => conversation.id);
6543
6680
  const allOnPageSelected = pageIds.length > 0 && pageIds.every((id) => selectedIds.has(id));
6544
- const toggleSelectAllOnPage = useCallback14(() => {
6681
+ const toggleSelectAllOnPage = useCallback15(() => {
6545
6682
  setSelectedIds((current) => {
6546
6683
  const next = new Set(current);
6547
6684
  if (allOnPageSelected) pageIds.forEach((id) => next.delete(id));
@@ -6549,7 +6686,7 @@ function useConversationsInbox(params = {}) {
6549
6686
  return next;
6550
6687
  });
6551
6688
  }, [allOnPageSelected, pageIds]);
6552
- const runOnSelection = useCallback14(
6689
+ const runOnSelection = useCallback15(
6553
6690
  async (action) => {
6554
6691
  setBusy(true);
6555
6692
  try {
@@ -6562,7 +6699,7 @@ function useConversationsInbox(params = {}) {
6562
6699
  },
6563
6700
  [selectedIds, refetch]
6564
6701
  );
6565
- const runConversationAction = useCallback14(
6702
+ const runConversationAction = useCallback15(
6566
6703
  async (action, conversationId) => {
6567
6704
  if (!action) return;
6568
6705
  setBusy(true);
@@ -6685,10 +6822,10 @@ function ConversationsWorkspace({
6685
6822
  ...markReadOnOpen === void 0 ? {} : { markReadOnOpen },
6686
6823
  ...serverPaginated ? { serverPaginated } : {}
6687
6824
  });
6688
- const [simulatorOpen, setSimulatorOpen] = useState25(false);
6689
- const [templateModalOpen, setTemplateModalOpen] = useState25(false);
6690
- const [openedFromLink, setOpenedFromLink] = useState25(void 0);
6691
- useEffect19(() => {
6825
+ const [simulatorOpen, setSimulatorOpen] = useState26(false);
6826
+ const [templateModalOpen, setTemplateModalOpen] = useState26(false);
6827
+ const [openedFromLink, setOpenedFromLink] = useState26(void 0);
6828
+ useEffect20(() => {
6692
6829
  const link = initialConversationId ?? initialWhatsappNumber;
6693
6830
  if (!link || openedFromLink === link) return;
6694
6831
  const target = inbox.conversations.find(
@@ -6708,11 +6845,11 @@ function ConversationsWorkspace({
6708
6845
  simulator && (simulator.enabled ?? true) && (simulator.render ?? transport)
6709
6846
  );
6710
6847
  const showSimulator = simulatorEnabled && simulatorOpen && Boolean(selected);
6711
- const simulatorClient = useMemo6(
6848
+ const simulatorClient = useMemo7(
6712
6849
  () => transport && selectedId ? transport({ conversationId: selectedId, channel, handle }) : void 0,
6713
6850
  [transport, selectedId, channel, handle]
6714
6851
  );
6715
- const paneUtilities = useMemo6(() => {
6852
+ const paneUtilities = useMemo7(() => {
6716
6853
  if (!selected) return void 0;
6717
6854
  const fromProduct = extraUtilitiesFor?.(selected) ?? [];
6718
6855
  if (!simulatorEnabled) return fromProduct;
@@ -7014,6 +7151,7 @@ export {
7014
7151
  buildTranscriptFilename,
7015
7152
  buildTranscriptText,
7016
7153
  capabilitiesOf,
7154
+ channelCapabilityFor,
7017
7155
  channelFiltersFor,
7018
7156
  contactFlag,
7019
7157
  createMediaUrlResolver,