@cossistant/react 0.0.20 → 0.0.23
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/conversation.d.ts +28 -0
- package/conversation.d.ts.map +1 -1
- package/hooks/index.d.ts +4 -1
- package/hooks/index.js +4 -1
- package/hooks/private/use-grouped-messages.d.ts.map +1 -1
- package/hooks/private/use-grouped-messages.js +3 -17
- package/hooks/private/use-grouped-messages.js.map +1 -1
- package/hooks/private/use-multimodal-input.d.ts.map +1 -1
- package/hooks/private/use-multimodal-input.js +16 -3
- package/hooks/private/use-multimodal-input.js.map +1 -1
- package/hooks/use-composer-refocus.js +1 -1
- package/hooks/use-composer-refocus.js.map +1 -1
- package/hooks/use-conversation-auto-seen.d.ts +1 -1
- package/hooks/use-conversation-auto-seen.js +30 -46
- package/hooks/use-conversation-auto-seen.js.map +1 -1
- package/hooks/use-conversation-seen.d.ts.map +1 -1
- package/hooks/use-conversation-seen.js +7 -3
- package/hooks/use-conversation-seen.js.map +1 -1
- package/hooks/use-new-message-sound.d.ts +23 -0
- package/hooks/use-new-message-sound.d.ts.map +1 -0
- package/hooks/use-new-message-sound.js +34 -0
- package/hooks/use-new-message-sound.js.map +1 -0
- package/hooks/use-sound-effect.d.ts +30 -0
- package/hooks/use-sound-effect.d.ts.map +1 -0
- package/hooks/use-sound-effect.js +104 -0
- package/hooks/use-sound-effect.js.map +1 -0
- package/hooks/use-typing-sound.d.ts +18 -0
- package/hooks/use-typing-sound.d.ts.map +1 -0
- package/hooks/use-typing-sound.js +38 -0
- package/hooks/use-typing-sound.js.map +1 -0
- package/index.d.ts +5 -2
- package/index.js +8 -6
- package/package.json +3 -3
- package/primitives/avatar/image.d.ts +1 -1
- package/primitives/bubble.js +1 -1
- package/primitives/index.d.ts +3 -5
- package/primitives/index.js +3 -9
- package/primitives/index.parts.d.ts +2 -4
- package/primitives/index.parts.js +2 -4
- package/primitives/router.d.ts +19 -20
- package/primitives/router.d.ts.map +1 -1
- package/primitives/router.js +17 -11
- package/primitives/router.js.map +1 -1
- package/realtime/index.js +1 -1
- package/realtime/provider.js +1 -1
- package/realtime-events.d.ts +83 -0
- package/realtime-events.d.ts.map +1 -1
- package/schemas3.d.ts +7 -0
- package/schemas3.d.ts.map +1 -1
- package/support/components/bubble.d.ts.map +1 -1
- package/support/components/bubble.js +27 -4
- package/support/components/bubble.js.map +1 -1
- package/support/components/button.d.ts +1 -1
- package/support/components/conversation-event.js +1 -1
- package/support/components/conversation-event.js.map +1 -1
- package/support/components/conversation-timeline.d.ts.map +1 -1
- package/support/components/conversation-timeline.js +5 -0
- package/support/components/conversation-timeline.js.map +1 -1
- package/support/components/multimodal-input.d.ts.map +1 -1
- package/support/components/multimodal-input.js +6 -2
- package/support/components/multimodal-input.js.map +1 -1
- package/support/components/support-content.d.ts +2 -0
- package/support/components/support-content.d.ts.map +1 -1
- package/support/components/support-content.js +5 -2
- package/support/components/support-content.js.map +1 -1
- package/support/components/timeline-message-group.js +2 -2
- package/support/components/timeline-message-group.js.map +1 -1
- package/support/components/timeline-message-item.js +2 -2
- package/support/components/timeline-message-item.js.map +1 -1
- package/support/index.d.ts +12 -7
- package/support/index.d.ts.map +1 -1
- package/support/index.js +28 -29
- package/support/index.js.map +1 -1
- package/support/pages/conversation.d.ts.map +1 -1
- package/support/pages/conversation.js +19 -1
- package/support/pages/conversation.js.map +1 -1
- package/support/router.d.ts +19 -9
- package/support/router.d.ts.map +1 -1
- package/support/router.js +31 -30
- package/support/router.js.map +1 -1
- package/timeline-item.d.ts +14 -0
- package/timeline-item.d.ts.map +1 -1
- package/utils/use-render-element.d.ts.map +1 -1
- package/primitives/page-registry.d.ts +0 -30
- package/primitives/page-registry.d.ts.map +0 -1
- package/primitives/page-registry.js +0 -45
- package/primitives/page-registry.js.map +0 -1
- package/primitives/page.d.ts +0 -21
- package/primitives/page.d.ts.map +0 -1
- package/primitives/page.js +0 -18
- package/primitives/page.js.map +0 -1
package/conversation.d.ts
CHANGED
|
@@ -62,6 +62,13 @@ declare const createConversationResponseSchema: ZodObject<{
|
|
|
62
62
|
mediaType: ZodString;
|
|
63
63
|
fileName: ZodOptional<ZodString>;
|
|
64
64
|
size: ZodOptional<ZodNumber>;
|
|
65
|
+
}, $strip>, ZodObject<{
|
|
66
|
+
type: ZodLiteral<"metadata">;
|
|
67
|
+
source: ZodEnum<{
|
|
68
|
+
email: "email";
|
|
69
|
+
widget: "widget";
|
|
70
|
+
api: "api";
|
|
71
|
+
}>;
|
|
65
72
|
}, $strip>]>>;
|
|
66
73
|
userId: ZodNullable<ZodString>;
|
|
67
74
|
aiAgentId: ZodNullable<ZodString>;
|
|
@@ -137,6 +144,13 @@ declare const createConversationResponseSchema: ZodObject<{
|
|
|
137
144
|
mediaType: ZodString;
|
|
138
145
|
fileName: ZodOptional<ZodString>;
|
|
139
146
|
size: ZodOptional<ZodNumber>;
|
|
147
|
+
}, $strip>, ZodObject<{
|
|
148
|
+
type: ZodLiteral<"metadata">;
|
|
149
|
+
source: ZodEnum<{
|
|
150
|
+
email: "email";
|
|
151
|
+
widget: "widget";
|
|
152
|
+
api: "api";
|
|
153
|
+
}>;
|
|
140
154
|
}, $strip>]>>;
|
|
141
155
|
userId: ZodNullable<ZodString>;
|
|
142
156
|
aiAgentId: ZodNullable<ZodString>;
|
|
@@ -234,6 +248,13 @@ declare const listConversationsResponseSchema: ZodObject<{
|
|
|
234
248
|
mediaType: ZodString;
|
|
235
249
|
fileName: ZodOptional<ZodString>;
|
|
236
250
|
size: ZodOptional<ZodNumber>;
|
|
251
|
+
}, $strip>, ZodObject<{
|
|
252
|
+
type: ZodLiteral<"metadata">;
|
|
253
|
+
source: ZodEnum<{
|
|
254
|
+
email: "email";
|
|
255
|
+
widget: "widget";
|
|
256
|
+
api: "api";
|
|
257
|
+
}>;
|
|
237
258
|
}, $strip>]>>;
|
|
238
259
|
userId: ZodNullable<ZodString>;
|
|
239
260
|
aiAgentId: ZodNullable<ZodString>;
|
|
@@ -324,6 +345,13 @@ declare const getConversationResponseSchema: ZodObject<{
|
|
|
324
345
|
mediaType: ZodString;
|
|
325
346
|
fileName: ZodOptional<ZodString>;
|
|
326
347
|
size: ZodOptional<ZodNumber>;
|
|
348
|
+
}, $strip>, ZodObject<{
|
|
349
|
+
type: ZodLiteral<"metadata">;
|
|
350
|
+
source: ZodEnum<{
|
|
351
|
+
email: "email";
|
|
352
|
+
widget: "widget";
|
|
353
|
+
api: "api";
|
|
354
|
+
}>;
|
|
327
355
|
}, $strip>]>>;
|
|
328
356
|
userId: ZodNullable<ZodString>;
|
|
329
357
|
aiAgentId: ZodNullable<ZodString>;
|
package/conversation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.d.ts","names":[],"sources":["../../types/src/api/conversation.ts"],"sourcesContent":[],"mappings":";;;;;;;;cA8Ba,kCAAgC
|
|
1
|
+
{"version":3,"file":"conversation.d.ts","names":[],"sources":["../../types/src/api/conversation.ts"],"sourcesContent":[],"mappings":";;;;;;;;cA8Ba,kCAAgC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAAA,OAAA,EAAA,SAAA;MAAA,CAAA,CAAA;MASjC,IAAA,SAAA,CAAA;QAIC,OAAA,EAAA,SAAA;;;;;;;;;;;;;;UAA8B,qBAAA,EAAA,uBAAA;UAAA,kBAAA,EAAA,oBAAA;UA6B/B,gBAAwB,EAAA,kBAC5B;UAGK,cAAA,EAAA,gBAaV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAlDS,8BAAA,GAAiC,cACrC;cAGK,gCAA8B;;;;;;;;;;;;;;;;;KA6B/B,wBAAA,GAA2B,cAC/B;cAGK,iCAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAAA,IAAA,WAAA;MAAA,CAAA,QAAA,CAAA,WAAA,CAAA;QAehC,IAAA,YAAyB,CAAA,OAAA,CAAA;QAIxB,SAAA,SAQV,CAAA;;;UARsC,qBAAA,EAAA,uBAAA;UAAA,kBAAA,EAAA,oBAAA;UAU7B,gBAAsB,EAAA,kBAC1B;UAGK,cAAA,EAMV,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAxBS,yBAAA,GAA4B,cAChC;cAGK,8BAA4B;;;KAU7B,sBAAA,GAAyB,cAC7B;cAGK,+BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAAA,IAAA,YAAA,CAAA,MAAA,CAAA;QAAA,IAAA,WAAA;MAQ9B,CAAA,QAAA,CAAA,WAAuB,CAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KADI,uBAAA,GAA0B,cAC9B"}
|
package/hooks/index.d.ts
CHANGED
|
@@ -18,9 +18,12 @@ import { UseConversationsOptions, UseConversationsResult, useConversations } fro
|
|
|
18
18
|
import { CreateConversationVariables, UseCreateConversationOptions, UseCreateConversationResult, useCreateConversation } from "./use-create-conversation.js";
|
|
19
19
|
import { UseHomePageOptions, UseHomePageReturn, useHomePage } from "./use-home-page.js";
|
|
20
20
|
import { UseMessageComposerOptions, UseMessageComposerReturn, useMessageComposer } from "./use-message-composer.js";
|
|
21
|
+
import { useNewMessageSound } from "./use-new-message-sound.js";
|
|
21
22
|
import { UseRealtimeSupportOptions, UseRealtimeSupportResult, useRealtimeSupport } from "./use-realtime-support.js";
|
|
22
23
|
import { UseScrollMaskOptions, UseScrollMaskReturn, useScrollMask } from "./use-scroll-mask.js";
|
|
23
24
|
import { SendMessageOptions, SendMessageResult, UseSendMessageOptions, UseSendMessageResult, useSendMessage } from "./use-send-message.js";
|
|
25
|
+
import { UseSoundEffectOptions, UseSoundEffectReturn, useSoundEffect } from "./use-sound-effect.js";
|
|
26
|
+
import { useTypingSound } from "./use-typing-sound.js";
|
|
24
27
|
import { UseVisitorReturn, useVisitor } from "./use-visitor.js";
|
|
25
28
|
import { WindowVisibilityFocusState, useWindowVisibilityFocus } from "./use-window-visibility-focus.js";
|
|
26
|
-
export { CONVERSATION_AUTO_SEEN_DELAY_MS, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CreateConversationVariables, GroupedMessage, SendMessageOptions, SendMessageResult, TimelineEventItem, TimelineToolItem, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseVisitorReturn, WindowVisibilityFocusState, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useRealtimeSupport, useScrollMask, useSendMessage, useVisitor, useWindowVisibilityFocus };
|
|
29
|
+
export { CONVERSATION_AUTO_SEEN_DELAY_MS, ConversationItem, ConversationLifecycleState, ConversationPreviewAssignedAgent, ConversationPreviewLastMessage, ConversationPreviewTypingParticipant, ConversationPreviewTypingState, ConversationTimelineTypingParticipant, ConversationTypingParticipant, CreateConversationVariables, GroupedMessage, SendMessageOptions, SendMessageResult, TimelineEventItem, TimelineToolItem, UseClientResult, UseComposerRefocusOptions, UseComposerRefocusReturn, UseConversationAutoSeenOptions, UseConversationHistoryPageOptions, UseConversationHistoryPageReturn, UseConversationLifecycleOptions, UseConversationLifecycleReturn, UseConversationOptions, UseConversationPageOptions, UseConversationPageReturn, UseConversationPreviewOptions, UseConversationPreviewReturn, UseConversationResult, UseConversationTimelineItemsOptions, UseConversationTimelineItemsResult, UseConversationTimelineOptions, UseConversationTimelineReturn, UseConversationsOptions, UseConversationsResult, UseCreateConversationOptions, UseCreateConversationResult, UseGroupedMessagesOptions, UseGroupedMessagesProps, UseHomePageOptions, UseHomePageReturn, UseMessageComposerOptions, UseMessageComposerReturn, UseMultimodalInputOptions, UseMultimodalInputReturn, UseRealtimeSupportOptions, UseRealtimeSupportResult, UseScrollMaskOptions, UseScrollMaskReturn, UseSendMessageOptions, UseSendMessageResult, UseSoundEffectOptions, UseSoundEffectReturn, UseVisitorReturn, WindowVisibilityFocusState, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
package/hooks/index.js
CHANGED
|
@@ -10,10 +10,13 @@ import { useMultimodalInput } from "./private/use-multimodal-input.js";
|
|
|
10
10
|
import { useSendMessage } from "./use-send-message.js";
|
|
11
11
|
import { useMessageComposer } from "./use-message-composer.js";
|
|
12
12
|
import { useConversationPage } from "./use-conversation-page.js";
|
|
13
|
+
import { useSoundEffect } from "./use-sound-effect.js";
|
|
14
|
+
import { useNewMessageSound } from "./use-new-message-sound.js";
|
|
13
15
|
import { useGroupedMessages } from "./private/use-grouped-messages.js";
|
|
14
16
|
import { useConversationSeen, useDebouncedConversationSeen } from "./use-conversation-seen.js";
|
|
15
17
|
import { useConversationTyping } from "./use-conversation-typing.js";
|
|
16
18
|
import { useConversationTimeline } from "./use-conversation-timeline.js";
|
|
19
|
+
import { useTypingSound } from "./use-typing-sound.js";
|
|
17
20
|
import { useComposerRefocus } from "./use-composer-refocus.js";
|
|
18
21
|
import { useVisitor } from "./use-visitor.js";
|
|
19
22
|
import { useConversations } from "./use-conversations.js";
|
|
@@ -24,4 +27,4 @@ import { useDefaultMessages } from "./private/use-default-messages.js";
|
|
|
24
27
|
import { useCreateConversation } from "./use-create-conversation.js";
|
|
25
28
|
import { useRealtimeSupport } from "./use-realtime-support.js";
|
|
26
29
|
|
|
27
|
-
export { CONVERSATION_AUTO_SEEN_DELAY_MS, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useRealtimeSupport, useScrollMask, useSendMessage, useVisitor, useWindowVisibilityFocus };
|
|
30
|
+
export { CONVERSATION_AUTO_SEEN_DELAY_MS, useClient, useClientQuery, useComposerRefocus, useConversation, useConversationAutoSeen, useConversationHistoryPage, useConversationLifecycle, useConversationPage, useConversationPreview, useConversationSeen, useConversationTimeline, useConversationTimelineItems, useConversationTyping, useConversations, useCreateConversation, useDebouncedConversationSeen, useDefaultMessages, useGroupedMessages, useHomePage, useMessageComposer, useMultimodalInput, useNewMessageSound, useRealtimeSupport, useScrollMask, useSendMessage, useSoundEffect, useTypingSound, useVisitor, useWindowVisibilityFocus };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-grouped-messages.d.ts","names":[],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":[],"mappings":";;;;;KAKY,cAAA;;EAAA,QAAA,EAAA,MAAA;EAGC,UAAA,EAAA,UAAA;EACL,KAAA,EAAA,YAAA,EAAA;EAGW,cAAA,EAAA,MAAA;EACD,aAAA,EAAA,MAAA;EAAI,gBAAA,EADH,IACG;EAGV,eAAA,EAHM,IAGW;AAM7B,CAAA;AAOY,KAbA,iBAAA,GAagB;EACzB,IAAA,EAAA,gBAAA;EACA,IAAA,EAbI,YAaJ;EACA,SAAA,EAbS,IAaT;CAAgB;AAEP,KAZA,gBAAA,GAYA;EACJ,IAAA,EAAA,eAAA;EACI,IAAA,EAZL,YAYK;EAEE,IAAA,EAAA,MAAA,GAAA,IAAA;EAAU,SAAA,EAZZ,IAYY;AAGxB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-grouped-messages.d.ts","names":[],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":[],"mappings":";;;;;KAKY,cAAA;;EAAA,QAAA,EAAA,MAAA;EAGC,UAAA,EAAA,UAAA;EACL,KAAA,EAAA,YAAA,EAAA;EAGW,cAAA,EAAA,MAAA;EACD,aAAA,EAAA,MAAA;EAAI,gBAAA,EADH,IACG;EAGV,eAAA,EAHM,IAGW;AAM7B,CAAA;AAOY,KAbA,iBAAA,GAagB;EACzB,IAAA,EAAA,gBAAA;EACA,IAAA,EAbI,YAaJ;EACA,SAAA,EAbS,IAaT;CAAgB;AAEP,KAZA,gBAAA,GAYA;EACJ,IAAA,EAAA,eAAA;EACI,IAAA,EAZL,YAYK;EAEE,IAAA,EAAA,MAAA,GAAA,IAAA;EAAU,SAAA,EAZZ,IAYY;AAGxB,CAAA;AA+La,KA3MD,gBAAA,GACT,cA8QF,GA7QE,iBA6QF,GA5QE,gBA4QF;AApEkC,KAtMvB,yBAAA,GAsMuB;EAAA,KAAA,EArM3B,YAqM2B,EAAA;EAAA,QAAA,CAAA,EApMvB,gBAoMuB,EAAA;EAAA,eAAA,CAAA,EAAA,MAAA;EAKhC,UAAA,CAAA,EAvMW,UAuMX;;KApMS,uBAAA,GAA0B;;;;;;;cA+LzB;;;;;GAKV"}
|
|
@@ -89,32 +89,18 @@ const buildTimelineReadReceiptData = (seenData, items) => {
|
|
|
89
89
|
for (const item of items) if (item.type === "message" && item.id) seenByMap.set(item.id, /* @__PURE__ */ new Set());
|
|
90
90
|
const sortedItems = [...items].filter((item) => item.type === "message").sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));
|
|
91
91
|
for (const seen of seenData) {
|
|
92
|
-
|
|
92
|
+
const seenTime = getTimestamp(seen.lastSeenAt);
|
|
93
93
|
const viewerId = seen.userId || seen.visitorId || seen.aiAgentId;
|
|
94
94
|
if (!viewerId) continue;
|
|
95
|
-
const lastItemByViewer = sortedItems.filter((item) => {
|
|
96
|
-
if (seen.userId) return item.userId === viewerId;
|
|
97
|
-
if (seen.visitorId) return item.visitorId === viewerId;
|
|
98
|
-
if (seen.aiAgentId) return item.aiAgentId === viewerId;
|
|
99
|
-
return false;
|
|
100
|
-
}).at(-1);
|
|
101
|
-
if (lastItemByViewer) {
|
|
102
|
-
const lastItemTime = getTimestamp(lastItemByViewer.createdAt);
|
|
103
|
-
if (lastItemTime > seenTime) seenTime = lastItemTime;
|
|
104
|
-
}
|
|
105
95
|
let lastReadItem = null;
|
|
106
96
|
let unreadCount = 0;
|
|
107
|
-
|
|
108
|
-
for (const item of sortedItems) if (getTimestamp(item.createdAt) <= seenTime && !hasPassedLastSeen) {
|
|
97
|
+
for (const item of sortedItems) if (getTimestamp(item.createdAt) <= seenTime) {
|
|
109
98
|
if (item.id) {
|
|
110
99
|
const seenBy = seenByMap.get(item.id);
|
|
111
100
|
if (seenBy) seenBy.add(viewerId);
|
|
112
101
|
}
|
|
113
102
|
lastReadItem = item;
|
|
114
|
-
} else
|
|
115
|
-
hasPassedLastSeen = true;
|
|
116
|
-
unreadCount++;
|
|
117
|
-
}
|
|
103
|
+
} else unreadCount++;
|
|
118
104
|
if (lastReadItem?.id) lastReadMessageMap.set(viewerId, lastReadItem.id);
|
|
119
105
|
unreadCountMap.set(viewerId, unreadCount);
|
|
120
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-grouped-messages.js","names":["EMPTY_STRING_ARRAY: readonly string[]","result: ConversationItem[]","currentGroup: GroupedMessage | null","lastReadItem: TimelineItem | null"],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":["import { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useMemo } from \"react\";\n\nexport type GroupedMessage = {\n\ttype: \"message_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstMessageId: string;\n\tlastMessageId: string;\n\tfirstMessageTime: Date;\n\tlastMessageTime: Date;\n};\n\nexport type TimelineEventItem = {\n\ttype: \"timeline_event\";\n\titem: TimelineItem;\n\ttimestamp: Date;\n};\n\nexport type TimelineToolItem = {\n\ttype: \"timeline_tool\";\n\titem: TimelineItem;\n\ttool: string | null;\n\ttimestamp: Date;\n};\n\nexport type ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem;\n\nexport type UseGroupedMessagesOptions = {\n\titems: TimelineItem[];\n\tseenData?: ConversationSeen[];\n\tcurrentViewerId?: string; // The ID of the current viewer (visitor, user, or AI agent)\n\tviewerType?: SenderType; // Type of the current viewer\n};\n\nexport type UseGroupedMessagesProps = UseGroupedMessagesOptions;\n\n// Helper function to safely get timestamp from Date or string\nconst getTimestamp = (date: Date | string | null | undefined): number => {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date).getTime();\n\t}\n\treturn date.getTime();\n};\n\n// Helper function to safely convert to Date\nconst toDate = (date: Date | string | null | undefined): Date => {\n\tif (!date) {\n\t\treturn typeof window !== \"undefined\" ? new Date() : new Date(0);\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date);\n\t}\n\treturn date;\n};\n\n// Helper to determine sender ID and type from a timeline item\nconst getSenderIdAndTypeFromTimelineItem = (\n\titem: TimelineItem\n): { senderId: string; senderType: SenderType } => {\n\tif (item.visitorId) {\n\t\treturn { senderId: item.visitorId, senderType: SenderType.VISITOR };\n\t}\n\tif (item.aiAgentId) {\n\t\treturn { senderId: item.aiAgentId, senderType: SenderType.AI };\n\t}\n\tif (item.userId) {\n\t\treturn { senderId: item.userId, senderType: SenderType.TEAM_MEMBER };\n\t}\n\n\t// Fallback\n\treturn {\n\t\tsenderId: item.id || \"default-sender\",\n\t\tsenderType: SenderType.TEAM_MEMBER,\n\t};\n};\n\nconst EMPTY_STRING_ARRAY: readonly string[] = Object.freeze([]);\n\n// Helper function to group timeline items (messages only, events stay separate)\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\n\tfor (const item of items) {\n\t\t// Events don't get grouped\n\t\tif (item.type === \"event\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add event as standalone item\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_event\",\n\t\t\t\titem,\n\t\t\t\ttimestamp: toDate(item.createdAt),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (item.type === \"identification\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add tool item as standalone entry\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_tool\",\n\t\t\t\titem,\n\t\t\t\ttool: item.tool ?? null,\n\t\t\t\ttimestamp: toDate(item.createdAt),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Group messages by sender\n\t\tconst { senderId, senderType } = getSenderIdAndTypeFromTimelineItem(item);\n\n\t\tif (currentGroup && currentGroup.senderId === senderId) {\n\t\t\t// Add to existing group\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = toDate(item.createdAt);\n\t\t} else {\n\t\t\t// Finalize previous group if exists\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t}\n\n\t\t\t// Start new group\n\t\t\tcurrentGroup = {\n\t\t\t\ttype: \"message_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstMessageId: item.id || \"\",\n\t\t\t\tlastMessageId: item.id || \"\",\n\t\t\t\tfirstMessageTime: toDate(item.createdAt),\n\t\t\t\tlastMessageTime: toDate(item.createdAt),\n\t\t\t};\n\t\t}\n\t}\n\n\tif (currentGroup) {\n\t\tresult.push(currentGroup);\n\t}\n\n\treturn result;\n};\n\n// Build read receipt data for timeline items\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[]\n) => {\n\tconst seenByMap = new Map<string, Set<string>>();\n\tconst lastReadMessageMap = new Map<string, string>();\n\tconst unreadCountMap = new Map<string, number>();\n\n\t// Initialize map for all message-type timeline items\n\tfor (const item of items) {\n\t\tif (item.type === \"message\" && item.id) {\n\t\t\tseenByMap.set(item.id, new Set());\n\t\t}\n\t}\n\n\t// Sort items by time to process in order\n\tconst sortedItems = [...items]\n\t\t.filter((item) => item.type === \"message\")\n\t\t.sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tlet seenTime = getTimestamp(seen.updatedAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Find the last message sent by this viewer\n\t\tconst lastItemByViewer = sortedItems\n\t\t\t.filter((item) => {\n\t\t\t\tif (seen.userId) {\n\t\t\t\t\treturn item.userId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.visitorId) {\n\t\t\t\t\treturn item.visitorId === viewerId;\n\t\t\t\t}\n\t\t\t\tif (seen.aiAgentId) {\n\t\t\t\t\treturn item.aiAgentId === viewerId;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t})\n\t\t\t.at(-1);\n\n\t\tif (lastItemByViewer) {\n\t\t\tconst lastItemTime = getTimestamp(lastItemByViewer.createdAt);\n\t\t\tif (lastItemTime > seenTime) {\n\t\t\t\tseenTime = lastItemTime;\n\t\t\t}\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\t\tlet hasPassedLastSeen = false;\n\n\t\t// Process items in chronological order\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime && !hasPassedLastSeen) {\n\t\t\t\t// This item has been seen\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tif (seenBy) {\n\t\t\t\t\t\tseenBy.add(viewerId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t} else {\n\t\t\t\t// This item is unread\n\t\t\t\thasPassedLastSeen = true;\n\t\t\t\tunreadCount++;\n\t\t\t}\n\t\t}\n\n\t\t// Store the last read item for this viewer\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\t// Store unread count\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n\tviewerType,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Build read receipt data\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(seenData, items);\n\n\t\t// Cache for turning seen sets into stable arrays across renders\n\t\tconst seenByArrayCache = new Map<string, readonly string[]>();\n\n\t\treturn {\n\t\t\titems: groupedItems,\n\t\t\tseenByMap,\n\t\t\tlastReadMessageMap,\n\t\t\tunreadCountMap,\n\n\t\t\tisMessageSeenByViewer: (messageId: string): boolean => {\n\t\t\t\tif (!currentViewerId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\treturn seenBy ? seenBy.has(currentViewerId) : false;\n\t\t\t},\n\n\t\t\tgetMessageSeenBy: (messageId: string): readonly string[] => {\n\t\t\t\tif (seenByArrayCache.has(messageId)) {\n\t\t\t\t\treturn seenByArrayCache.get(messageId) ?? EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\tif (!seenBy || seenBy.size === 0) {\n\t\t\t\t\tseenByArrayCache.set(messageId, EMPTY_STRING_ARRAY);\n\t\t\t\t\treturn EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst result = Object.freeze(Array.from(seenBy)) as readonly string[];\n\t\t\t\tseenByArrayCache.set(messageId, result);\n\t\t\t\treturn result;\n\t\t\t},\n\n\t\t\tgetLastReadMessageId: (userId: string): string | undefined =>\n\t\t\t\tlastReadMessageMap.get(userId),\n\n\t\t\tisLastReadMessage: (messageId: string, userId: string): boolean =>\n\t\t\t\tlastReadMessageMap.get(userId) === messageId,\n\n\t\t\tgetUnreadCount: (userId: string): number =>\n\t\t\t\tunreadCountMap.get(userId) || 0,\n\n\t\t\thasUnreadAfter: (messageId: string, userId: string): boolean => {\n\t\t\t\tconst lastRead = lastReadMessageMap.get(userId);\n\t\t\t\tif (!lastRead) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tconst messageIndex = items.findIndex((item) => item.id === messageId);\n\t\t\t\tconst lastReadIndex = items.findIndex((item) => item.id === lastRead);\n\n\t\t\t\treturn messageIndex < lastReadIndex;\n\t\t\t},\n\t\t};\n\t}, [items, seenData, currentViewerId]);\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-grouped-messages.js","names":["EMPTY_STRING_ARRAY: readonly string[]","result: ConversationItem[]","currentGroup: GroupedMessage | null","lastReadItem: TimelineItem | null"],"sources":["../../../src/hooks/private/use-grouped-messages.ts"],"sourcesContent":["import { SenderType } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useMemo } from \"react\";\n\nexport type GroupedMessage = {\n\ttype: \"message_group\";\n\tsenderId: string;\n\tsenderType: SenderType;\n\titems: TimelineItem[];\n\tfirstMessageId: string;\n\tlastMessageId: string;\n\tfirstMessageTime: Date;\n\tlastMessageTime: Date;\n};\n\nexport type TimelineEventItem = {\n\ttype: \"timeline_event\";\n\titem: TimelineItem;\n\ttimestamp: Date;\n};\n\nexport type TimelineToolItem = {\n\ttype: \"timeline_tool\";\n\titem: TimelineItem;\n\ttool: string | null;\n\ttimestamp: Date;\n};\n\nexport type ConversationItem =\n\t| GroupedMessage\n\t| TimelineEventItem\n\t| TimelineToolItem;\n\nexport type UseGroupedMessagesOptions = {\n\titems: TimelineItem[];\n\tseenData?: ConversationSeen[];\n\tcurrentViewerId?: string; // The ID of the current viewer (visitor, user, or AI agent)\n\tviewerType?: SenderType; // Type of the current viewer\n};\n\nexport type UseGroupedMessagesProps = UseGroupedMessagesOptions;\n\n// Helper function to safely get timestamp from Date or string\nconst getTimestamp = (date: Date | string | null | undefined): number => {\n\tif (!date) {\n\t\treturn 0;\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date).getTime();\n\t}\n\treturn date.getTime();\n};\n\n// Helper function to safely convert to Date\nconst toDate = (date: Date | string | null | undefined): Date => {\n\tif (!date) {\n\t\treturn typeof window !== \"undefined\" ? new Date() : new Date(0);\n\t}\n\tif (typeof date === \"string\") {\n\t\treturn new Date(date);\n\t}\n\treturn date;\n};\n\n// Helper to determine sender ID and type from a timeline item\nconst getSenderIdAndTypeFromTimelineItem = (\n\titem: TimelineItem\n): { senderId: string; senderType: SenderType } => {\n\tif (item.visitorId) {\n\t\treturn { senderId: item.visitorId, senderType: SenderType.VISITOR };\n\t}\n\tif (item.aiAgentId) {\n\t\treturn { senderId: item.aiAgentId, senderType: SenderType.AI };\n\t}\n\tif (item.userId) {\n\t\treturn { senderId: item.userId, senderType: SenderType.TEAM_MEMBER };\n\t}\n\n\t// Fallback\n\treturn {\n\t\tsenderId: item.id || \"default-sender\",\n\t\tsenderType: SenderType.TEAM_MEMBER,\n\t};\n};\n\nconst EMPTY_STRING_ARRAY: readonly string[] = Object.freeze([]);\n\n// Helper function to group timeline items (messages only, events stay separate)\nconst groupTimelineItems = (items: TimelineItem[]): ConversationItem[] => {\n\tconst result: ConversationItem[] = [];\n\tlet currentGroup: GroupedMessage | null = null;\n\n\tfor (const item of items) {\n\t\t// Events don't get grouped\n\t\tif (item.type === \"event\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add event as standalone item\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_event\",\n\t\t\t\titem,\n\t\t\t\ttimestamp: toDate(item.createdAt),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (item.type === \"identification\") {\n\t\t\t// Finalize any existing group\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t\tcurrentGroup = null;\n\t\t\t}\n\n\t\t\t// Add tool item as standalone entry\n\t\t\tresult.push({\n\t\t\t\ttype: \"timeline_tool\",\n\t\t\t\titem,\n\t\t\t\ttool: item.tool ?? null,\n\t\t\t\ttimestamp: toDate(item.createdAt),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Group messages by sender\n\t\tconst { senderId, senderType } = getSenderIdAndTypeFromTimelineItem(item);\n\n\t\tif (currentGroup && currentGroup.senderId === senderId) {\n\t\t\t// Add to existing group\n\t\t\tcurrentGroup.items.push(item);\n\t\t\tcurrentGroup.lastMessageId = item.id || currentGroup.lastMessageId;\n\t\t\tcurrentGroup.lastMessageTime = toDate(item.createdAt);\n\t\t} else {\n\t\t\t// Finalize previous group if exists\n\t\t\tif (currentGroup) {\n\t\t\t\tresult.push(currentGroup);\n\t\t\t}\n\n\t\t\t// Start new group\n\t\t\tcurrentGroup = {\n\t\t\t\ttype: \"message_group\",\n\t\t\t\tsenderId,\n\t\t\t\tsenderType,\n\t\t\t\titems: [item],\n\t\t\t\tfirstMessageId: item.id || \"\",\n\t\t\t\tlastMessageId: item.id || \"\",\n\t\t\t\tfirstMessageTime: toDate(item.createdAt),\n\t\t\t\tlastMessageTime: toDate(item.createdAt),\n\t\t\t};\n\t\t}\n\t}\n\n\tif (currentGroup) {\n\t\tresult.push(currentGroup);\n\t}\n\n\treturn result;\n};\n\n// Build read receipt data for timeline items\nconst buildTimelineReadReceiptData = (\n\tseenData: ConversationSeen[],\n\titems: TimelineItem[]\n) => {\n\tconst seenByMap = new Map<string, Set<string>>();\n\tconst lastReadMessageMap = new Map<string, string>();\n\tconst unreadCountMap = new Map<string, number>();\n\n\t// Initialize map for all message-type timeline items\n\tfor (const item of items) {\n\t\tif (item.type === \"message\" && item.id) {\n\t\t\tseenByMap.set(item.id, new Set());\n\t\t}\n\t}\n\n\t// Sort items by time to process in order\n\tconst sortedItems = [...items]\n\t\t.filter((item) => item.type === \"message\")\n\t\t.sort((a, b) => getTimestamp(a.createdAt) - getTimestamp(b.createdAt));\n\n\t// Process seen data for each viewer\n\tfor (const seen of seenData) {\n\t\tconst seenTime = getTimestamp(seen.lastSeenAt);\n\t\tconst viewerId = seen.userId || seen.visitorId || seen.aiAgentId;\n\t\tif (!viewerId) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet lastReadItem: TimelineItem | null = null;\n\t\tlet unreadCount = 0;\n\n\t\t// Process items in chronological order\n\t\tfor (const item of sortedItems) {\n\t\t\tconst itemTime = getTimestamp(item.createdAt);\n\n\t\t\tif (itemTime <= seenTime) {\n\t\t\t\t// This item has been seen\n\t\t\t\tif (item.id) {\n\t\t\t\t\tconst seenBy = seenByMap.get(item.id);\n\t\t\t\t\tif (seenBy) {\n\t\t\t\t\t\tseenBy.add(viewerId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlastReadItem = item;\n\t\t\t} else {\n\t\t\t\t// This item is unread\n\t\t\t\tunreadCount++;\n\t\t\t}\n\t\t}\n\n\t\t// Store the last read item for this viewer\n\t\tif (lastReadItem?.id) {\n\t\t\tlastReadMessageMap.set(viewerId, lastReadItem.id);\n\t\t}\n\n\t\t// Store unread count\n\t\tunreadCountMap.set(viewerId, unreadCount);\n\t}\n\n\treturn { seenByMap, lastReadMessageMap, unreadCountMap };\n};\n\n/**\n * Batches sequential timeline items from the same sender into groups and enriches\n * them with read-receipt helpers so UIs can render conversation timelines with\n * minimal effort. Seen data is normalised into quick lookup maps for unread\n * indicators.\n */\nexport const useGroupedMessages = ({\n\titems,\n\tseenData = [],\n\tcurrentViewerId,\n\tviewerType,\n}: UseGroupedMessagesOptions) => {\n\treturn useMemo(() => {\n\t\tconst groupedItems = groupTimelineItems(items);\n\n\t\t// Build read receipt data\n\t\tconst { seenByMap, lastReadMessageMap, unreadCountMap } =\n\t\t\tbuildTimelineReadReceiptData(seenData, items);\n\n\t\t// Cache for turning seen sets into stable arrays across renders\n\t\tconst seenByArrayCache = new Map<string, readonly string[]>();\n\n\t\treturn {\n\t\t\titems: groupedItems,\n\t\t\tseenByMap,\n\t\t\tlastReadMessageMap,\n\t\t\tunreadCountMap,\n\n\t\t\tisMessageSeenByViewer: (messageId: string): boolean => {\n\t\t\t\tif (!currentViewerId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\treturn seenBy ? seenBy.has(currentViewerId) : false;\n\t\t\t},\n\n\t\t\tgetMessageSeenBy: (messageId: string): readonly string[] => {\n\t\t\t\tif (seenByArrayCache.has(messageId)) {\n\t\t\t\t\treturn seenByArrayCache.get(messageId) ?? EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst seenBy = seenByMap.get(messageId);\n\t\t\t\tif (!seenBy || seenBy.size === 0) {\n\t\t\t\t\tseenByArrayCache.set(messageId, EMPTY_STRING_ARRAY);\n\t\t\t\t\treturn EMPTY_STRING_ARRAY;\n\t\t\t\t}\n\n\t\t\t\tconst result = Object.freeze(Array.from(seenBy)) as readonly string[];\n\t\t\t\tseenByArrayCache.set(messageId, result);\n\t\t\t\treturn result;\n\t\t\t},\n\n\t\t\tgetLastReadMessageId: (userId: string): string | undefined =>\n\t\t\t\tlastReadMessageMap.get(userId),\n\n\t\t\tisLastReadMessage: (messageId: string, userId: string): boolean =>\n\t\t\t\tlastReadMessageMap.get(userId) === messageId,\n\n\t\t\tgetUnreadCount: (userId: string): number =>\n\t\t\t\tunreadCountMap.get(userId) || 0,\n\n\t\t\thasUnreadAfter: (messageId: string, userId: string): boolean => {\n\t\t\t\tconst lastRead = lastReadMessageMap.get(userId);\n\t\t\t\tif (!lastRead) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\n\t\t\t\tconst messageIndex = items.findIndex((item) => item.id === messageId);\n\t\t\t\tconst lastReadIndex = items.findIndex((item) => item.id === lastRead);\n\n\t\t\t\treturn messageIndex < lastReadIndex;\n\t\t\t},\n\t\t};\n\t}, [items, seenData, currentViewerId]);\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-multimodal-input.d.ts","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":[],"mappings":";KAEY,yBAAA;EAAA,QAAA,CAAA,EAAA,CAAA,IAAA,EAAA;IACiC,OAAA,EAAA,MAAA;IAAoB,KAAA,EAApB,IAAoB,EAAA;EAC9C,CAAA,EAAA,GAAA,IAAA,GAD8C,OAC9C,CAAA,IAAA,CAAA;EAAK,OAAA,CAAA,EAAA,CAAA,KAAA,EAAL,KAAK,EAAA,GAAA,IAAA;EAMZ,WAAA,CAAA,EAAA,MAAA;EAGJ,QAAA,CAAA,EAAA,MAAA;EAEA,gBAAA,CAAA,EAAA,MAAA,EAAA;CAIW;AAGJ,KAZH,wBAAA,GAYG;EAAO,OAAA,EAAA,MAAA;EAaT,KAAA,EAtBL,IAsBK,EAAA;EAAsB,YAAA,EAAA,OAAA;EAAA,KAAA,EApB3B,KAoB2B,GAAA,IAAA;EAAA,UAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAA,QAAA,EAAA,CAAA,KAAA,EAhBhB,IAgBgB,EAAA,EAAA,GAAA,IAAA;EAAA,UAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAMhC,UAAA,EAAA,GAAA,GAAA,IAAA;EAAiC,MAAA,EAAA,GAAA,GAnBrB,OAmBqB,CAAA,IAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"use-multimodal-input.d.ts","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":[],"mappings":";KAEY,yBAAA;EAAA,QAAA,CAAA,EAAA,CAAA,IAAA,EAAA;IACiC,OAAA,EAAA,MAAA;IAAoB,KAAA,EAApB,IAAoB,EAAA;EAC9C,CAAA,EAAA,GAAA,IAAA,GAD8C,OAC9C,CAAA,IAAA,CAAA;EAAK,OAAA,CAAA,EAAA,CAAA,KAAA,EAAL,KAAK,EAAA,GAAA,IAAA;EAMZ,WAAA,CAAA,EAAA,MAAA;EAGJ,QAAA,CAAA,EAAA,MAAA;EAEA,gBAAA,CAAA,EAAA,MAAA,EAAA;CAIW;AAGJ,KAZH,wBAAA,GAYG;EAAO,OAAA,EAAA,MAAA;EAaT,KAAA,EAtBL,IAsBK,EAAA;EAAsB,YAAA,EAAA,OAAA;EAAA,KAAA,EApB3B,KAoB2B,GAAA,IAAA;EAAA,UAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAAA,QAAA,EAAA,CAAA,KAAA,EAhBhB,IAgBgB,EAAA,EAAA,GAAA,IAAA;EAAA,UAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EAMhC,UAAA,EAAA,GAAA,GAAA,IAAA;EAAiC,MAAA,EAAA,GAAA,GAnBrB,OAmBqB,CAAA,IAAA,CAAA;EAwKnC,KAAA,EAAA,GAAA,GAAA,IAAA;;;;;;;;;cA9KY;;;;;;IAMV,8BAAiC"}
|
|
@@ -79,21 +79,34 @@ const useMultimodalInput = ({ onSubmit, onError, maxFileSize = 10 * 1024 * 1024,
|
|
|
79
79
|
}, [clearFiles]);
|
|
80
80
|
const submit = useCallback(async () => {
|
|
81
81
|
if (!onSubmit) return;
|
|
82
|
-
|
|
82
|
+
const trimmedMessage = message.trim();
|
|
83
|
+
if (!trimmedMessage && files.length === 0) {
|
|
83
84
|
const err = /* @__PURE__ */ new Error("Please provide a message or attach files");
|
|
84
85
|
setError(err);
|
|
85
86
|
onError?.(err);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
const previousState = {
|
|
90
|
+
message,
|
|
91
|
+
files,
|
|
92
|
+
fileUrls: [...fileUrlsRef.current]
|
|
93
|
+
};
|
|
88
94
|
setIsSubmitting(true);
|
|
89
95
|
setError(null);
|
|
96
|
+
setMessage("");
|
|
97
|
+
setFiles([]);
|
|
98
|
+
fileUrlsRef.current = [];
|
|
90
99
|
try {
|
|
91
100
|
await onSubmit({
|
|
92
|
-
message:
|
|
93
|
-
files
|
|
101
|
+
message: trimmedMessage,
|
|
102
|
+
files: previousState.files
|
|
94
103
|
});
|
|
104
|
+
for (const url of previousState.fileUrls) URL.revokeObjectURL(url);
|
|
95
105
|
reset();
|
|
96
106
|
} catch (err) {
|
|
107
|
+
setMessage(previousState.message);
|
|
108
|
+
setFiles(previousState.files);
|
|
109
|
+
fileUrlsRef.current = previousState.fileUrls;
|
|
97
110
|
const _error = err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to submit");
|
|
98
111
|
setError(_error);
|
|
99
112
|
onError?.(_error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-multimodal-input.js","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\n\nexport type UseMultimodalInputOptions = {\n\tonSubmit?: (data: { message: string; files: File[] }) => void | Promise<void>;\n\tonError?: (error: Error) => void;\n\tmaxFileSize?: number; // in bytes\n\tmaxFiles?: number;\n\tallowedFileTypes?: string[]; // MIME types\n};\n\nexport type UseMultimodalInputReturn = {\n\t// State\n\tmessage: string;\n\tfiles: File[];\n\tisSubmitting: boolean;\n\terror: Error | null;\n\n\t// Actions\n\tsetMessage: (message: string) => void;\n\taddFiles: (files: File[]) => void;\n\tremoveFile: (index: number) => void;\n\tclearFiles: () => void;\n\tsubmit: () => Promise<void>;\n\treset: () => void;\n\n\t// Validation\n\tisValid: boolean;\n\tcanSubmit: boolean;\n};\n\n/**\n * Manages message text, file attachments and validation for the multimodal\n * composer component. Provides ergonomic helpers for submit flows and error\n * reporting.\n */\nexport const useMultimodalInput = ({\n\tonSubmit,\n\tonError,\n\tmaxFileSize = 10 * 1024 * 1024, // 10MB default\n\tmaxFiles = 5,\n\tallowedFileTypes = [\"image/*\", \"application/pdf\", \"text/*\"],\n}: UseMultimodalInputOptions = {}): UseMultimodalInputReturn => {\n\tconst [message, setMessage] = useState(\"\");\n\tconst [files, setFiles] = useState<File[]>([]);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\t// Use ref to prevent re-renders when tracking file URLs\n\tconst fileUrlsRef = useRef<string[]>([]);\n\n\t// Validation helpers\n\tconst validateFile = useCallback(\n\t\t(file: File): string | null => {\n\t\t\tif (file.size > maxFileSize) {\n\t\t\t\treturn `File \"${file.name}\" exceeds maximum size of ${maxFileSize / 1024 / 1024}MB`;\n\t\t\t}\n\n\t\t\tif (allowedFileTypes.length > 0) {\n\t\t\t\tconst isAllowed = allowedFileTypes.some((type) => {\n\t\t\t\t\tif (type.endsWith(\"/*\")) {\n\t\t\t\t\t\tconst baseType = type.slice(0, -2);\n\t\t\t\t\t\treturn file.type.startsWith(baseType);\n\t\t\t\t\t}\n\t\t\t\t\treturn file.type === type;\n\t\t\t\t});\n\n\t\t\t\tif (!isAllowed) {\n\t\t\t\t\treturn `File type \"${file.type}\" is not allowed`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\t\t[maxFileSize, allowedFileTypes]\n\t);\n\n\t// Actions\n\tconst addFiles = useCallback(\n\t\t(newFiles: File[]) => {\n\t\t\tsetError(null);\n\n\t\t\t// Check max files limit\n\t\t\tif (files.length + newFiles.length > maxFiles) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot add files: maximum ${maxFiles} files allowed`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate each file\n\t\t\tfor (const file of newFiles) {\n\t\t\t\tconst validationError = validateFile(file);\n\t\t\t\tif (validationError) {\n\t\t\t\t\tconst err = new Error(validationError);\n\t\t\t\t\tsetError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsetFiles((prev) => [...prev, ...newFiles]);\n\t\t},\n\t\t[files.length, maxFiles, validateFile, onError]\n\t);\n\n\tconst removeFile = useCallback((index: number) => {\n\t\tsetFiles((prev) => {\n\t\t\tconst newFiles = [...prev];\n\t\t\tnewFiles.splice(index, 1);\n\n\t\t\t// Clean up object URL if it exists\n\t\t\tif (fileUrlsRef.current[index]) {\n\t\t\t\tURL.revokeObjectURL(fileUrlsRef.current[index]);\n\t\t\t\tfileUrlsRef.current.splice(index, 1);\n\t\t\t}\n\n\t\t\treturn newFiles;\n\t\t});\n\t\tsetError(null);\n\t}, []);\n\n\tconst clearFiles = useCallback(() => {\n\t\t// Clean up all object URLs\n\t\tfor (const url of fileUrlsRef.current) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tfileUrlsRef.current = [];\n\n\t\tsetFiles([]);\n\t\tsetError(null);\n\t}, []);\n\n\tconst reset = useCallback(() => {\n\t\tsetMessage(\"\");\n\t\tclearFiles();\n\t\tsetError(null);\n\t\tsetIsSubmitting(false);\n\t}, [clearFiles]);\n\n\tconst submit = useCallback(async () => {\n\t\tif (!onSubmit) {\n\t\t\treturn;\n\t\t}\n\n\t\
|
|
1
|
+
{"version":3,"file":"use-multimodal-input.js","names":[],"sources":["../../../src/hooks/private/use-multimodal-input.ts"],"sourcesContent":["import { useCallback, useRef, useState } from \"react\";\n\nexport type UseMultimodalInputOptions = {\n\tonSubmit?: (data: { message: string; files: File[] }) => void | Promise<void>;\n\tonError?: (error: Error) => void;\n\tmaxFileSize?: number; // in bytes\n\tmaxFiles?: number;\n\tallowedFileTypes?: string[]; // MIME types\n};\n\nexport type UseMultimodalInputReturn = {\n\t// State\n\tmessage: string;\n\tfiles: File[];\n\tisSubmitting: boolean;\n\terror: Error | null;\n\n\t// Actions\n\tsetMessage: (message: string) => void;\n\taddFiles: (files: File[]) => void;\n\tremoveFile: (index: number) => void;\n\tclearFiles: () => void;\n\tsubmit: () => Promise<void>;\n\treset: () => void;\n\n\t// Validation\n\tisValid: boolean;\n\tcanSubmit: boolean;\n};\n\n/**\n * Manages message text, file attachments and validation for the multimodal\n * composer component. Provides ergonomic helpers for submit flows and error\n * reporting.\n */\nexport const useMultimodalInput = ({\n\tonSubmit,\n\tonError,\n\tmaxFileSize = 10 * 1024 * 1024, // 10MB default\n\tmaxFiles = 5,\n\tallowedFileTypes = [\"image/*\", \"application/pdf\", \"text/*\"],\n}: UseMultimodalInputOptions = {}): UseMultimodalInputReturn => {\n\tconst [message, setMessage] = useState(\"\");\n\tconst [files, setFiles] = useState<File[]>([]);\n\tconst [isSubmitting, setIsSubmitting] = useState(false);\n\tconst [error, setError] = useState<Error | null>(null);\n\n\t// Use ref to prevent re-renders when tracking file URLs\n\tconst fileUrlsRef = useRef<string[]>([]);\n\n\t// Validation helpers\n\tconst validateFile = useCallback(\n\t\t(file: File): string | null => {\n\t\t\tif (file.size > maxFileSize) {\n\t\t\t\treturn `File \"${file.name}\" exceeds maximum size of ${maxFileSize / 1024 / 1024}MB`;\n\t\t\t}\n\n\t\t\tif (allowedFileTypes.length > 0) {\n\t\t\t\tconst isAllowed = allowedFileTypes.some((type) => {\n\t\t\t\t\tif (type.endsWith(\"/*\")) {\n\t\t\t\t\t\tconst baseType = type.slice(0, -2);\n\t\t\t\t\t\treturn file.type.startsWith(baseType);\n\t\t\t\t\t}\n\t\t\t\t\treturn file.type === type;\n\t\t\t\t});\n\n\t\t\t\tif (!isAllowed) {\n\t\t\t\t\treturn `File type \"${file.type}\" is not allowed`;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn null;\n\t\t},\n\t\t[maxFileSize, allowedFileTypes]\n\t);\n\n\t// Actions\n\tconst addFiles = useCallback(\n\t\t(newFiles: File[]) => {\n\t\t\tsetError(null);\n\n\t\t\t// Check max files limit\n\t\t\tif (files.length + newFiles.length > maxFiles) {\n\t\t\t\tconst err = new Error(\n\t\t\t\t\t`Cannot add files: maximum ${maxFiles} files allowed`\n\t\t\t\t);\n\t\t\t\tsetError(err);\n\t\t\t\tonError?.(err);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Validate each file\n\t\t\tfor (const file of newFiles) {\n\t\t\t\tconst validationError = validateFile(file);\n\t\t\t\tif (validationError) {\n\t\t\t\t\tconst err = new Error(validationError);\n\t\t\t\t\tsetError(err);\n\t\t\t\t\tonError?.(err);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsetFiles((prev) => [...prev, ...newFiles]);\n\t\t},\n\t\t[files.length, maxFiles, validateFile, onError]\n\t);\n\n\tconst removeFile = useCallback((index: number) => {\n\t\tsetFiles((prev) => {\n\t\t\tconst newFiles = [...prev];\n\t\t\tnewFiles.splice(index, 1);\n\n\t\t\t// Clean up object URL if it exists\n\t\t\tif (fileUrlsRef.current[index]) {\n\t\t\t\tURL.revokeObjectURL(fileUrlsRef.current[index]);\n\t\t\t\tfileUrlsRef.current.splice(index, 1);\n\t\t\t}\n\n\t\t\treturn newFiles;\n\t\t});\n\t\tsetError(null);\n\t}, []);\n\n\tconst clearFiles = useCallback(() => {\n\t\t// Clean up all object URLs\n\t\tfor (const url of fileUrlsRef.current) {\n\t\t\tURL.revokeObjectURL(url);\n\t\t}\n\t\tfileUrlsRef.current = [];\n\n\t\tsetFiles([]);\n\t\tsetError(null);\n\t}, []);\n\n\tconst reset = useCallback(() => {\n\t\tsetMessage(\"\");\n\t\tclearFiles();\n\t\tsetError(null);\n\t\tsetIsSubmitting(false);\n\t}, [clearFiles]);\n\n\tconst submit = useCallback(async () => {\n\t\tif (!onSubmit) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst trimmedMessage = message.trim();\n\t\tif (!trimmedMessage && files.length === 0) {\n\t\t\tconst err = new Error(\"Please provide a message or attach files\");\n\t\t\tsetError(err);\n\t\t\tonError?.(err);\n\t\t\treturn;\n\t\t}\n\n\t\tconst previousState = {\n\t\t\tmessage,\n\t\t\tfiles,\n\t\t\tfileUrls: [...fileUrlsRef.current],\n\t\t};\n\n\t\tsetIsSubmitting(true);\n\t\tsetError(null);\n\t\tsetMessage(\"\");\n\t\tsetFiles([]);\n\t\tfileUrlsRef.current = [];\n\n\t\ttry {\n\t\t\tawait onSubmit({ message: trimmedMessage, files: previousState.files });\n\n\t\t\tfor (const url of previousState.fileUrls) {\n\t\t\t\tURL.revokeObjectURL(url);\n\t\t\t}\n\t\t\treset();\n\t\t} catch (err) {\n\t\t\tsetMessage(previousState.message);\n\t\t\tsetFiles(previousState.files);\n\t\t\tfileUrlsRef.current = previousState.fileUrls;\n\n\t\t\tconst _error = err instanceof Error ? err : new Error(\"Failed to submit\");\n\t\t\tsetError(_error);\n\t\t\tonError?.(_error);\n\t\t} finally {\n\t\t\tsetIsSubmitting(false);\n\t\t}\n\t}, [message, files, onSubmit, onError, reset]);\n\n\t// Computed values\n\tconst isValid = message.trim().length > 0 || files.length > 0;\n\tconst canSubmit = isValid && !isSubmitting && !error;\n\n\treturn {\n\t\t// State\n\t\tmessage,\n\t\tfiles,\n\t\tisSubmitting,\n\t\terror,\n\n\t\t// Actions\n\t\tsetMessage,\n\t\taddFiles,\n\t\tremoveFile,\n\t\tclearFiles,\n\t\tsubmit,\n\t\treset,\n\n\t\t// Validation\n\t\tisValid,\n\t\tcanSubmit,\n\t};\n};\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ const useComposerRefocus = ({ disabled, hasContent, isSubmitting }) => {
|
|
|
9
9
|
});
|
|
10
10
|
useEffect(() => {
|
|
11
11
|
const previous = previousStateRef.current;
|
|
12
|
-
if (!(disabled ||
|
|
12
|
+
if (!(disabled || hasContent) && (previous.isSubmitting || previous.hadContent)) inputRef.current?.focus();
|
|
13
13
|
previousStateRef.current = {
|
|
14
14
|
isSubmitting,
|
|
15
15
|
hadContent: hasContent
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-composer-refocus.js","names":[],"sources":["../../src/hooks/use-composer-refocus.ts"],"sourcesContent":["import type { MutableRefObject } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nexport type UseComposerRefocusOptions = {\n\tdisabled: boolean;\n\thasContent: boolean;\n\tisSubmitting: boolean;\n};\n\nexport type UseComposerRefocusReturn = {\n\tfocusComposer: () => void;\n\tinputRef: MutableRefObject<HTMLTextAreaElement | null>;\n};\n\nexport const useComposerRefocus = ({\n\tdisabled,\n\thasContent,\n\tisSubmitting,\n}: UseComposerRefocusOptions): UseComposerRefocusReturn => {\n\tconst inputRef = useRef<HTMLTextAreaElement | null>(null);\n\tconst previousStateRef = useRef({\n\t\tisSubmitting,\n\t\thadContent: hasContent,\n\t});\n\n\tuseEffect(() => {\n\t\tconst previous = previousStateRef.current;\n\n\t\tif (\n\t\t\t!(disabled ||
|
|
1
|
+
{"version":3,"file":"use-composer-refocus.js","names":[],"sources":["../../src/hooks/use-composer-refocus.ts"],"sourcesContent":["import type { MutableRefObject } from \"react\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nexport type UseComposerRefocusOptions = {\n\tdisabled: boolean;\n\thasContent: boolean;\n\tisSubmitting: boolean;\n};\n\nexport type UseComposerRefocusReturn = {\n\tfocusComposer: () => void;\n\tinputRef: MutableRefObject<HTMLTextAreaElement | null>;\n};\n\nexport const useComposerRefocus = ({\n\tdisabled,\n\thasContent,\n\tisSubmitting,\n}: UseComposerRefocusOptions): UseComposerRefocusReturn => {\n\tconst inputRef = useRef<HTMLTextAreaElement | null>(null);\n\tconst previousStateRef = useRef({\n\t\tisSubmitting,\n\t\thadContent: hasContent,\n\t});\n\n\tuseEffect(() => {\n\t\tconst previous = previousStateRef.current;\n\n\t\tif (\n\t\t\t!(disabled || hasContent) &&\n\t\t\t(previous.isSubmitting || previous.hadContent)\n\t\t) {\n\t\t\tinputRef.current?.focus();\n\t\t}\n\n\t\tpreviousStateRef.current = {\n\t\t\tisSubmitting,\n\t\t\thadContent: hasContent,\n\t\t};\n\t}, [disabled, hasContent, isSubmitting]);\n\n\tconst focusComposer = useCallback(() => {\n\t\tinputRef.current?.focus();\n\t}, []);\n\n\treturn {\n\t\tfocusComposer,\n\t\tinputRef,\n\t};\n};\n"],"mappings":";;;AAcA,MAAa,sBAAsB,EAClC,UACA,YACA,mBAC0D;CAC1D,MAAM,WAAW,OAAmC,KAAK;CACzD,MAAM,mBAAmB,OAAO;EAC/B;EACA,YAAY;EACZ,CAAC;AAEF,iBAAgB;EACf,MAAM,WAAW,iBAAiB;AAElC,MACC,EAAE,YAAY,gBACb,SAAS,gBAAgB,SAAS,YAEnC,UAAS,SAAS,OAAO;AAG1B,mBAAiB,UAAU;GAC1B;GACA,YAAY;GACZ;IACC;EAAC;EAAU;EAAY;EAAa,CAAC;AAMxC,QAAO;EACN,eALqB,kBAAkB;AACvC,YAAS,SAAS,OAAO;KACvB,EAAE,CAAC;EAIL;EACA"}
|
|
@@ -37,7 +37,7 @@ type UseConversationAutoSeenOptions = {
|
|
|
37
37
|
/**
|
|
38
38
|
* Automatically marks timeline items as seen when:
|
|
39
39
|
* - A new timeline item arrives from someone else
|
|
40
|
-
* - The page is visible
|
|
40
|
+
* - The page is visible
|
|
41
41
|
* - The support widget is open/visible
|
|
42
42
|
* - The visitor is the current user
|
|
43
43
|
*
|
|
@@ -7,7 +7,7 @@ const CONVERSATION_AUTO_SEEN_DELAY_MS = 2e3;
|
|
|
7
7
|
/**
|
|
8
8
|
* Automatically marks timeline items as seen when:
|
|
9
9
|
* - A new timeline item arrives from someone else
|
|
10
|
-
* - The page is visible
|
|
10
|
+
* - The page is visible
|
|
11
11
|
* - The support widget is open/visible
|
|
12
12
|
* - The visitor is the current user
|
|
13
13
|
*
|
|
@@ -32,26 +32,7 @@ function useConversationAutoSeen(options) {
|
|
|
32
32
|
const lastSeenItemIdRef = useRef(null);
|
|
33
33
|
const markSeenInFlightRef = useRef(false);
|
|
34
34
|
const markSeenTimeoutRef = useRef(null);
|
|
35
|
-
const { isPageVisible
|
|
36
|
-
const latestStateRef = useRef({
|
|
37
|
-
enabled,
|
|
38
|
-
isWidgetOpen,
|
|
39
|
-
isPageVisible,
|
|
40
|
-
hasWindowFocus
|
|
41
|
-
});
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
latestStateRef.current = {
|
|
44
|
-
enabled,
|
|
45
|
-
isWidgetOpen,
|
|
46
|
-
isPageVisible,
|
|
47
|
-
hasWindowFocus
|
|
48
|
-
};
|
|
49
|
-
}, [
|
|
50
|
-
enabled,
|
|
51
|
-
isWidgetOpen,
|
|
52
|
-
hasWindowFocus,
|
|
53
|
-
isPageVisible
|
|
54
|
-
]);
|
|
35
|
+
const { isPageVisible } = useWindowVisibilityFocus();
|
|
55
36
|
useEffect(() => {
|
|
56
37
|
lastSeenItemIdRef.current = null;
|
|
57
38
|
markSeenInFlightRef.current = false;
|
|
@@ -82,7 +63,7 @@ function useConversationAutoSeen(options) {
|
|
|
82
63
|
conversationId
|
|
83
64
|
]);
|
|
84
65
|
useEffect(() => {
|
|
85
|
-
if (!(isWidgetOpen &&
|
|
66
|
+
if (!(enabled && isWidgetOpen && client && conversationId && visitorId && lastTimelineItem && isPageVisible)) {
|
|
86
67
|
if (markSeenTimeoutRef.current) {
|
|
87
68
|
clearTimeout(markSeenTimeoutRef.current);
|
|
88
69
|
markSeenTimeoutRef.current = null;
|
|
@@ -93,36 +74,40 @@ function useConversationAutoSeen(options) {
|
|
|
93
74
|
clearTimeout(markSeenTimeoutRef.current);
|
|
94
75
|
markSeenTimeoutRef.current = null;
|
|
95
76
|
}
|
|
96
|
-
if (!(client && conversationId && visitorId && lastTimelineItem && isPageVisible && hasWindowFocus)) return;
|
|
97
77
|
if (lastTimelineItem.visitorId === visitorId) {
|
|
98
78
|
lastSeenItemIdRef.current = lastTimelineItem.id || null;
|
|
99
79
|
return;
|
|
100
80
|
}
|
|
101
81
|
if (lastSeenItemIdRef.current === lastTimelineItem.id) return;
|
|
102
|
-
if (markSeenInFlightRef.current) return;
|
|
103
82
|
const pendingItemId = lastTimelineItem.id || null;
|
|
104
83
|
markSeenTimeoutRef.current = setTimeout(() => {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
84
|
+
const attemptMarkSeen = () => {
|
|
85
|
+
if (!(enabled && isWidgetOpen && client && conversationId && visitorId && isPageVisible)) {
|
|
86
|
+
markSeenInFlightRef.current = false;
|
|
87
|
+
markSeenTimeoutRef.current = null;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (markSeenInFlightRef.current) {
|
|
91
|
+
markSeenTimeoutRef.current = setTimeout(attemptMarkSeen, 100);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
markSeenInFlightRef.current = true;
|
|
95
|
+
client.markConversationSeen({ conversationId }).then((response) => {
|
|
96
|
+
lastSeenItemIdRef.current = pendingItemId;
|
|
97
|
+
upsertConversationSeen({
|
|
98
|
+
conversationId,
|
|
99
|
+
actorType: "visitor",
|
|
100
|
+
actorId: visitorId,
|
|
101
|
+
lastSeenAt: new Date(response.lastSeenAt)
|
|
102
|
+
});
|
|
103
|
+
}).catch((err) => {
|
|
104
|
+
console.error("Failed to mark conversation as seen:", err);
|
|
105
|
+
}).finally(() => {
|
|
106
|
+
markSeenInFlightRef.current = false;
|
|
107
|
+
markSeenTimeoutRef.current = null;
|
|
119
108
|
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
}).finally(() => {
|
|
123
|
-
markSeenInFlightRef.current = false;
|
|
124
|
-
markSeenTimeoutRef.current = null;
|
|
125
|
-
});
|
|
109
|
+
};
|
|
110
|
+
attemptMarkSeen();
|
|
126
111
|
}, CONVERSATION_AUTO_SEEN_DELAY_MS);
|
|
127
112
|
return () => {
|
|
128
113
|
if (markSeenTimeoutRef.current) {
|
|
@@ -137,8 +122,7 @@ function useConversationAutoSeen(options) {
|
|
|
137
122
|
conversationId,
|
|
138
123
|
visitorId,
|
|
139
124
|
lastTimelineItem,
|
|
140
|
-
isPageVisible
|
|
141
|
-
hasWindowFocus
|
|
125
|
+
isPageVisible
|
|
142
126
|
]);
|
|
143
127
|
}
|
|
144
128
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-auto-seen.js","names":[],"sources":["../../src/hooks/use-conversation-auto-seen.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useEffect, useRef } from \"react\";\nimport {\n\thydrateConversationSeen,\n\tupsertConversationSeen,\n} from \"../realtime/seen-store\";\nimport { useWindowVisibilityFocus } from \"./use-window-visibility-focus\";\n\nexport const CONVERSATION_AUTO_SEEN_DELAY_MS = 2000;\n\nexport type UseConversationAutoSeenOptions = {\n\t/**\n\t * The Cossistant client instance.\n\t */\n\tclient: CossistantClient | null;\n\n\t/**\n\t * The real conversation ID. Pass null if no conversation exists yet.\n\t */\n\tconversationId: string | null;\n\n\t/**\n\t * Current visitor ID.\n\t */\n\tvisitorId?: string;\n\n\t/**\n\t * The last timeline item in the conversation.\n\t * Used to determine if we should mark as seen.\n\t */\n\tlastTimelineItem: TimelineItem | null;\n\n\t/**\n\t * Whether to enable auto-seen tracking.\n\t * Default: true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Whether the support widget is currently open/visible.\n\t * This is required to ensure we only mark conversations as seen when\n\t * the widget is actually visible to the user.\n\t * Default: true\n\t */\n\tisWidgetOpen?: boolean;\n};\n\n/**\n * Automatically marks timeline items as seen when:\n * - A new timeline item arrives from someone else\n * - The page is visible
|
|
1
|
+
{"version":3,"file":"use-conversation-auto-seen.js","names":[],"sources":["../../src/hooks/use-conversation-auto-seen.ts"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { useEffect, useRef } from \"react\";\nimport {\n\thydrateConversationSeen,\n\tupsertConversationSeen,\n} from \"../realtime/seen-store\";\nimport { useWindowVisibilityFocus } from \"./use-window-visibility-focus\";\n\nexport const CONVERSATION_AUTO_SEEN_DELAY_MS = 2000;\n\nexport type UseConversationAutoSeenOptions = {\n\t/**\n\t * The Cossistant client instance.\n\t */\n\tclient: CossistantClient | null;\n\n\t/**\n\t * The real conversation ID. Pass null if no conversation exists yet.\n\t */\n\tconversationId: string | null;\n\n\t/**\n\t * Current visitor ID.\n\t */\n\tvisitorId?: string;\n\n\t/**\n\t * The last timeline item in the conversation.\n\t * Used to determine if we should mark as seen.\n\t */\n\tlastTimelineItem: TimelineItem | null;\n\n\t/**\n\t * Whether to enable auto-seen tracking.\n\t * Default: true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * Whether the support widget is currently open/visible.\n\t * This is required to ensure we only mark conversations as seen when\n\t * the widget is actually visible to the user.\n\t * Default: true\n\t */\n\tisWidgetOpen?: boolean;\n};\n\n/**\n * Automatically marks timeline items as seen when:\n * - A new timeline item arrives from someone else\n * - The page is visible\n * - The support widget is open/visible\n * - The visitor is the current user\n *\n * Also handles:\n * - Fetching and hydrating initial seen data\n * - Preventing duplicate API calls\n * - Page visibility tracking\n * - Widget visibility tracking\n *\n * @example\n * ```tsx\n * useConversationAutoSeen({\n * client,\n * conversationId: realConversationId,\n * visitorId: visitor?.id,\n * lastTimelineItem: items[items.length - 1] ?? null,\n * });\n * ```\n */\nexport function useConversationAutoSeen(\n\toptions: UseConversationAutoSeenOptions\n): void {\n\tconst {\n\t\tclient,\n\t\tconversationId,\n\t\tvisitorId,\n\t\tlastTimelineItem,\n\t\tenabled = true,\n\t\tisWidgetOpen = true,\n\t} = options;\n\n\tconst lastSeenItemIdRef = useRef<string | null>(null);\n\tconst markSeenInFlightRef = useRef(false);\n\tconst markSeenTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\tconst { isPageVisible } = useWindowVisibilityFocus();\n\n\t// Reset seen tracking when conversation changes\n\tuseEffect(() => {\n\t\tlastSeenItemIdRef.current = null;\n\t\tmarkSeenInFlightRef.current = false;\n\t\tif (markSeenTimeoutRef.current) {\n\t\t\tclearTimeout(markSeenTimeoutRef.current);\n\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t}\n\t}, [conversationId]);\n\n\t// Clear timeout immediately when widget closes and reset tracking\n\tuseEffect(() => {\n\t\tif (!isWidgetOpen) {\n\t\t\tif (markSeenTimeoutRef.current) {\n\t\t\t\tclearTimeout(markSeenTimeoutRef.current);\n\t\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t\t}\n\t\t\tmarkSeenInFlightRef.current = false;\n\t\t\t// Reset last seen item ID so we don't skip marking when widget reopens\n\t\t\t// This ensures we check again when the widget is reopened\n\t\t\tlastSeenItemIdRef.current = null;\n\t\t}\n\t}, [isWidgetOpen]);\n\n\t// Fetch and hydrate initial seen data when conversation loads\n\tuseEffect(() => {\n\t\tif (enabled && client && conversationId) {\n\t\t\tvoid client\n\t\t\t\t.getConversationSeenData({ conversationId })\n\t\t\t\t.then((response) => {\n\t\t\t\t\tif (response.seenData.length > 0) {\n\t\t\t\t\t\thydrateConversationSeen(conversationId, response.seenData);\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tconsole.error(\"Failed to fetch conversation seen data:\", err);\n\t\t\t\t});\n\t\t}\n\t}, [enabled, client, conversationId]);\n\n\t// Auto-mark timeline items as seen\n\tuseEffect(() => {\n\t\tconst canMarkSeen =\n\t\t\tenabled &&\n\t\t\tisWidgetOpen &&\n\t\t\tclient &&\n\t\t\tconversationId &&\n\t\t\tvisitorId &&\n\t\t\tlastTimelineItem &&\n\t\t\tisPageVisible;\n\n\t\tif (!canMarkSeen) {\n\t\t\tif (markSeenTimeoutRef.current) {\n\t\t\t\tclearTimeout(markSeenTimeoutRef.current);\n\t\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (markSeenTimeoutRef.current) {\n\t\t\tclearTimeout(markSeenTimeoutRef.current);\n\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t}\n\n\t\t// Don't mark our own timeline items as seen via API (we already know we saw them)\n\t\tif (lastTimelineItem.visitorId === visitorId) {\n\t\t\tlastSeenItemIdRef.current = lastTimelineItem.id || null;\n\t\t\treturn;\n\t\t}\n\n\t\t// Already marked this item\n\t\tif (lastSeenItemIdRef.current === lastTimelineItem.id) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst pendingItemId = lastTimelineItem.id || null;\n\n\t\tmarkSeenTimeoutRef.current = setTimeout(() => {\n\t\t\tconst attemptMarkSeen = () => {\n\t\t\t\tconst stillCanMark =\n\t\t\t\t\tenabled &&\n\t\t\t\t\tisWidgetOpen &&\n\t\t\t\t\tclient &&\n\t\t\t\t\tconversationId &&\n\t\t\t\t\tvisitorId &&\n\t\t\t\t\tisPageVisible;\n\n\t\t\t\tif (!stillCanMark) {\n\t\t\t\t\tmarkSeenInFlightRef.current = false;\n\t\t\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (markSeenInFlightRef.current) {\n\t\t\t\t\tmarkSeenTimeoutRef.current = setTimeout(attemptMarkSeen, 100);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tmarkSeenInFlightRef.current = true;\n\n\t\t\t\tclient\n\t\t\t\t\t.markConversationSeen({ conversationId })\n\t\t\t\t\t.then((response) => {\n\t\t\t\t\t\tlastSeenItemIdRef.current = pendingItemId;\n\n\t\t\t\t\t\t// Optimistically update local seen store\n\t\t\t\t\t\tupsertConversationSeen({\n\t\t\t\t\t\t\tconversationId,\n\t\t\t\t\t\t\tactorType: \"visitor\",\n\t\t\t\t\t\t\tactorId: visitorId,\n\t\t\t\t\t\t\tlastSeenAt: new Date(response.lastSeenAt),\n\t\t\t\t\t\t});\n\t\t\t\t\t})\n\t\t\t\t\t.catch((err) => {\n\t\t\t\t\t\tconsole.error(\"Failed to mark conversation as seen:\", err);\n\t\t\t\t\t})\n\t\t\t\t\t.finally(() => {\n\t\t\t\t\t\tmarkSeenInFlightRef.current = false;\n\t\t\t\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t\t\t\t});\n\t\t\t};\n\n\t\t\tattemptMarkSeen();\n\t\t}, CONVERSATION_AUTO_SEEN_DELAY_MS);\n\n\t\treturn () => {\n\t\t\tif (markSeenTimeoutRef.current) {\n\t\t\t\tclearTimeout(markSeenTimeoutRef.current);\n\t\t\t\tmarkSeenTimeoutRef.current = null;\n\t\t\t}\n\t\t};\n\t}, [\n\t\tenabled,\n\t\tisWidgetOpen,\n\t\tclient,\n\t\tconversationId,\n\t\tvisitorId,\n\t\tlastTimelineItem,\n\t\tisPageVisible,\n\t]);\n}\n"],"mappings":";;;;;AASA,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;AA8D/C,SAAgB,wBACf,SACO;CACP,MAAM,EACL,QACA,gBACA,WACA,kBACA,UAAU,MACV,eAAe,SACZ;CAEJ,MAAM,oBAAoB,OAAsB,KAAK;CACrD,MAAM,sBAAsB,OAAO,MAAM;CACzC,MAAM,qBAAqB,OAA6C,KAAK;CAC7E,MAAM,EAAE,kBAAkB,0BAA0B;AAGpD,iBAAgB;AACf,oBAAkB,UAAU;AAC5B,sBAAoB,UAAU;AAC9B,MAAI,mBAAmB,SAAS;AAC/B,gBAAa,mBAAmB,QAAQ;AACxC,sBAAmB,UAAU;;IAE5B,CAAC,eAAe,CAAC;AAGpB,iBAAgB;AACf,MAAI,CAAC,cAAc;AAClB,OAAI,mBAAmB,SAAS;AAC/B,iBAAa,mBAAmB,QAAQ;AACxC,uBAAmB,UAAU;;AAE9B,uBAAoB,UAAU;AAG9B,qBAAkB,UAAU;;IAE3B,CAAC,aAAa,CAAC;AAGlB,iBAAgB;AACf,MAAI,WAAW,UAAU,eACxB,CAAK,OACH,wBAAwB,EAAE,gBAAgB,CAAC,CAC3C,MAAM,aAAa;AACnB,OAAI,SAAS,SAAS,SAAS,EAC9B,yBAAwB,gBAAgB,SAAS,SAAS;IAE1D,CACD,OAAO,QAAQ;AACf,WAAQ,MAAM,2CAA2C,IAAI;IAC5D;IAEF;EAAC;EAAS;EAAQ;EAAe,CAAC;AAGrC,iBAAgB;AAUf,MAAI,EARH,WACA,gBACA,UACA,kBACA,aACA,oBACA,gBAEiB;AACjB,OAAI,mBAAmB,SAAS;AAC/B,iBAAa,mBAAmB,QAAQ;AACxC,uBAAmB,UAAU;;AAE9B;;AAGD,MAAI,mBAAmB,SAAS;AAC/B,gBAAa,mBAAmB,QAAQ;AACxC,sBAAmB,UAAU;;AAI9B,MAAI,iBAAiB,cAAc,WAAW;AAC7C,qBAAkB,UAAU,iBAAiB,MAAM;AACnD;;AAID,MAAI,kBAAkB,YAAY,iBAAiB,GAClD;EAGD,MAAM,gBAAgB,iBAAiB,MAAM;AAE7C,qBAAmB,UAAU,iBAAiB;GAC7C,MAAM,wBAAwB;AAS7B,QAAI,EAPH,WACA,gBACA,UACA,kBACA,aACA,gBAEkB;AAClB,yBAAoB,UAAU;AAC9B,wBAAmB,UAAU;AAC7B;;AAGD,QAAI,oBAAoB,SAAS;AAChC,wBAAmB,UAAU,WAAW,iBAAiB,IAAI;AAC7D;;AAGD,wBAAoB,UAAU;AAE9B,WACE,qBAAqB,EAAE,gBAAgB,CAAC,CACxC,MAAM,aAAa;AACnB,uBAAkB,UAAU;AAG5B,4BAAuB;MACtB;MACA,WAAW;MACX,SAAS;MACT,YAAY,IAAI,KAAK,SAAS,WAAW;MACzC,CAAC;MACD,CACD,OAAO,QAAQ;AACf,aAAQ,MAAM,wCAAwC,IAAI;MACzD,CACD,cAAc;AACd,yBAAoB,UAAU;AAC9B,wBAAmB,UAAU;MAC5B;;AAGJ,oBAAiB;KACf,gCAAgC;AAEnC,eAAa;AACZ,OAAI,mBAAmB,SAAS;AAC/B,iBAAa,mBAAmB,QAAQ;AACxC,uBAAmB,UAAU;;;IAG7B;EACF;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-seen.d.ts","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":[],"mappings":";;;KAIK,0BAAA;gBACU;AALmD,CAAA;AAoBlE;
|
|
1
|
+
{"version":3,"file":"use-conversation-seen.d.ts","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":[],"mappings":";;;KAIK,0BAAA;gBACU;AALmD,CAAA;AAoBlE;AA8DA;;;iBA9DgB,mBAAA,sDAEN,6BACP;;;;;;;iBA2Da,4BAAA,sDAEN,6CAEP"}
|
|
@@ -13,12 +13,16 @@ function useConversationSeen(conversationId, options = {}) {
|
|
|
13
13
|
const { initialData } = options;
|
|
14
14
|
const hydratedKeyRef = useRef(null);
|
|
15
15
|
useEffect(() => {
|
|
16
|
-
if (!
|
|
17
|
-
|
|
16
|
+
if (!conversationId) {
|
|
17
|
+
hydratedKeyRef.current = null;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!initialData || initialData.length === 0) return;
|
|
21
|
+
const hydrationKey = conversationId;
|
|
18
22
|
if (hydratedKeyRef.current === hydrationKey) return;
|
|
19
23
|
hydrateConversationSeen(conversationId, initialData);
|
|
20
24
|
hydratedKeyRef.current = hydrationKey;
|
|
21
|
-
}, [conversationId
|
|
25
|
+
}, [conversationId]);
|
|
22
26
|
const conversationSeen = useSeenStore((state) => conversationId ? state.conversations[conversationId] ?? null : null);
|
|
23
27
|
return useMemo(() => {
|
|
24
28
|
if (!(conversationId && conversationSeen)) return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-conversation-seen.js","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":["import type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { hydrateConversationSeen, useSeenStore } from \"../realtime/seen-store\";\n\ntype UseConversationSeenOptions = {\n\tinitialData?: ConversationSeen[];\n};\n\nfunction buildSeenId(\n\tconversationId: string,\n\tactorType: string,\n\tactorId: string\n) {\n\treturn `${conversationId}-${actorType}-${actorId}`;\n}\n\n/**\n * Reads the conversation seen store and optionally hydrates it with SSR\n * payloads.\n */\nexport function useConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {}\n): ConversationSeen[] {\n\tconst { initialData } = options;\n\tconst hydratedKeyRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\
|
|
1
|
+
{"version":3,"file":"use-conversation-seen.js","names":[],"sources":["../../src/hooks/use-conversation-seen.ts"],"sourcesContent":["import type { ConversationSeen } from \"@cossistant/types/schemas\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { hydrateConversationSeen, useSeenStore } from \"../realtime/seen-store\";\n\ntype UseConversationSeenOptions = {\n\tinitialData?: ConversationSeen[];\n};\n\nfunction buildSeenId(\n\tconversationId: string,\n\tactorType: string,\n\tactorId: string\n) {\n\treturn `${conversationId}-${actorType}-${actorId}`;\n}\n\n/**\n * Reads the conversation seen store and optionally hydrates it with SSR\n * payloads.\n */\nexport function useConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {}\n): ConversationSeen[] {\n\tconst { initialData } = options;\n\tconst hydratedKeyRef = useRef<string | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear hydration key when conversation changes or is unmounted\n\t\tif (!conversationId) {\n\t\t\thydratedKeyRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\t// Skip if no initial data\n\t\tif (!initialData || initialData.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Only hydrate once per conversation\n\t\tconst hydrationKey = conversationId;\n\n\t\tif (hydratedKeyRef.current === hydrationKey) {\n\t\t\treturn; // Already hydrated for this conversation\n\t\t}\n\n\t\thydrateConversationSeen(conversationId, initialData);\n\t\thydratedKeyRef.current = hydrationKey;\n\t}, [conversationId]); // Only depend on conversationId, NOT initialData\n\n\tconst conversationSeen = useSeenStore((state) =>\n\t\tconversationId ? (state.conversations[conversationId] ?? null) : null\n\t);\n\n\treturn useMemo(() => {\n\t\tif (!(conversationId && conversationSeen)) {\n\t\t\treturn [];\n\t\t}\n\n\t\treturn Object.values(conversationSeen).map(\n\t\t\t(entry) =>\n\t\t\t\t({\n\t\t\t\t\tid: buildSeenId(conversationId, entry.actorType, entry.actorId),\n\t\t\t\t\tconversationId,\n\t\t\t\t\tuserId: entry.actorType === \"user\" ? entry.actorId : null,\n\t\t\t\t\tvisitorId: entry.actorType === \"visitor\" ? entry.actorId : null,\n\t\t\t\t\taiAgentId: entry.actorType === \"ai_agent\" ? entry.actorId : null,\n\t\t\t\t\tlastSeenAt: entry.lastSeenAt,\n\t\t\t\t\tcreatedAt: entry.lastSeenAt,\n\t\t\t\t\tupdatedAt: entry.lastSeenAt,\n\t\t\t\t\tdeletedAt: null,\n\t\t\t\t}) satisfies ConversationSeen\n\t\t);\n\t}, [conversationId, conversationSeen]);\n}\n\n/**\n * Debounced version of useConversationSeen that delays updates by 500ms\n * to prevent animation conflicts when messages are sent and immediately seen.\n *\n * Use this in UI components where smooth animations are critical.\n */\nexport function useDebouncedConversationSeen(\n\tconversationId: string | null | undefined,\n\toptions: UseConversationSeenOptions = {},\n\tdelay = 500\n): ConversationSeen[] {\n\tconst seenData = useConversationSeen(conversationId, options);\n\tconst [debouncedSeenData, setDebouncedSeenData] =\n\t\tuseState<ConversationSeen[]>(seenData);\n\tconst timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n\tuseEffect(() => {\n\t\t// Clear any pending timeout\n\t\tif (timeoutRef.current) {\n\t\t\tclearTimeout(timeoutRef.current);\n\t\t}\n\n\t\t// Set new timeout to update after delay\n\t\ttimeoutRef.current = setTimeout(() => {\n\t\t\tsetDebouncedSeenData(seenData);\n\t\t}, delay);\n\n\t\t// Cleanup on unmount or when seenData changes\n\t\treturn () => {\n\t\t\tif (timeoutRef.current) {\n\t\t\t\tclearTimeout(timeoutRef.current);\n\t\t\t}\n\t\t};\n\t}, [seenData, delay]);\n\n\treturn debouncedSeenData;\n}\n"],"mappings":";;;;AAQA,SAAS,YACR,gBACA,WACA,SACC;AACD,QAAO,GAAG,eAAe,GAAG,UAAU,GAAG;;;;;;AAO1C,SAAgB,oBACf,gBACA,UAAsC,EAAE,EACnB;CACrB,MAAM,EAAE,gBAAgB;CACxB,MAAM,iBAAiB,OAAsB,KAAK;AAElD,iBAAgB;AAEf,MAAI,CAAC,gBAAgB;AACpB,kBAAe,UAAU;AACzB;;AAID,MAAI,CAAC,eAAe,YAAY,WAAW,EAC1C;EAID,MAAM,eAAe;AAErB,MAAI,eAAe,YAAY,aAC9B;AAGD,0BAAwB,gBAAgB,YAAY;AACpD,iBAAe,UAAU;IACvB,CAAC,eAAe,CAAC;CAEpB,MAAM,mBAAmB,cAAc,UACtC,iBAAkB,MAAM,cAAc,mBAAmB,OAAQ,KACjE;AAED,QAAO,cAAc;AACpB,MAAI,EAAE,kBAAkB,kBACvB,QAAO,EAAE;AAGV,SAAO,OAAO,OAAO,iBAAiB,CAAC,KACrC,WACC;GACA,IAAI,YAAY,gBAAgB,MAAM,WAAW,MAAM,QAAQ;GAC/D;GACA,QAAQ,MAAM,cAAc,SAAS,MAAM,UAAU;GACrD,WAAW,MAAM,cAAc,YAAY,MAAM,UAAU;GAC3D,WAAW,MAAM,cAAc,aAAa,MAAM,UAAU;GAC5D,YAAY,MAAM;GAClB,WAAW,MAAM;GACjB,WAAW,MAAM;GACjB,WAAW;GACX,EACF;IACC,CAAC,gBAAgB,iBAAiB,CAAC;;;;;;;;AASvC,SAAgB,6BACf,gBACA,UAAsC,EAAE,EACxC,QAAQ,KACa;CACrB,MAAM,WAAW,oBAAoB,gBAAgB,QAAQ;CAC7D,MAAM,CAAC,mBAAmB,wBACzB,SAA6B,SAAS;CACvC,MAAM,aAAa,OAA8B,KAAK;AAEtD,iBAAgB;AAEf,MAAI,WAAW,QACd,cAAa,WAAW,QAAQ;AAIjC,aAAW,UAAU,iBAAiB;AACrC,wBAAqB,SAAS;KAC5B,MAAM;AAGT,eAAa;AACZ,OAAI,WAAW,QACd,cAAa,WAAW,QAAQ;;IAGhC,CAAC,UAAU,MAAM,CAAC;AAErB,QAAO"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/hooks/use-new-message-sound.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Hook to play a sound when a new message arrives.
|
|
4
|
+
*
|
|
5
|
+
* @param options - Optional configuration for volume and playback speed
|
|
6
|
+
* @returns Function to play the new message sound
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const playNewMessageSound = useNewMessageSound({ volume: 0.8, playbackRate: 1.1 });
|
|
10
|
+
*
|
|
11
|
+
* useEffect(() => {
|
|
12
|
+
* if (hasNewMessage) {
|
|
13
|
+
* playNewMessageSound();
|
|
14
|
+
* }
|
|
15
|
+
* }, [hasNewMessage]);
|
|
16
|
+
*/
|
|
17
|
+
declare function useNewMessageSound(options?: {
|
|
18
|
+
volume?: number;
|
|
19
|
+
playbackRate?: number;
|
|
20
|
+
}): () => void;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { useNewMessageSound };
|
|
23
|
+
//# sourceMappingURL=use-new-message-sound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-new-message-sound.d.ts","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":[],"mappings":";;AAqBA;;;;;;;;;;;;;;iBAAgB,kBAAA"}
|