@cossistant/react 0.0.20 → 0.0.22
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/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/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 +14 -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/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/components/typing-indicator.d.ts.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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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useSoundEffect } from "./use-sound-effect.js";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/hooks/use-new-message-sound.ts
|
|
5
|
+
const NEW_MESSAGE_SOUND_PATH = "/sounds/new-message.wav";
|
|
6
|
+
/**
|
|
7
|
+
* Hook to play a sound when a new message arrives.
|
|
8
|
+
*
|
|
9
|
+
* @param options - Optional configuration for volume and playback speed
|
|
10
|
+
* @returns Function to play the new message sound
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const playNewMessageSound = useNewMessageSound({ volume: 0.8, playbackRate: 1.1 });
|
|
14
|
+
*
|
|
15
|
+
* useEffect(() => {
|
|
16
|
+
* if (hasNewMessage) {
|
|
17
|
+
* playNewMessageSound();
|
|
18
|
+
* }
|
|
19
|
+
* }, [hasNewMessage]);
|
|
20
|
+
*/
|
|
21
|
+
function useNewMessageSound(options) {
|
|
22
|
+
const { play } = useSoundEffect(NEW_MESSAGE_SOUND_PATH, {
|
|
23
|
+
loop: false,
|
|
24
|
+
volume: options?.volume ?? .7,
|
|
25
|
+
playbackRate: options?.playbackRate ?? 1
|
|
26
|
+
});
|
|
27
|
+
return useCallback(() => {
|
|
28
|
+
play();
|
|
29
|
+
}, [play]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { useNewMessageSound };
|
|
34
|
+
//# sourceMappingURL=use-new-message-sound.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-new-message-sound.js","names":[],"sources":["../../src/hooks/use-new-message-sound.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useSoundEffect } from \"./use-sound-effect\";\n\n// Use a path that can be served from public directory\nconst NEW_MESSAGE_SOUND_PATH = \"/sounds/new-message.wav\";\n\n/**\n * Hook to play a sound when a new message arrives.\n *\n * @param options - Optional configuration for volume and playback speed\n * @returns Function to play the new message sound\n *\n * @example\n * const playNewMessageSound = useNewMessageSound({ volume: 0.8, playbackRate: 1.1 });\n *\n * useEffect(() => {\n * if (hasNewMessage) {\n * playNewMessageSound();\n * }\n * }, [hasNewMessage]);\n */\nexport function useNewMessageSound(options?: {\n\tvolume?: number;\n\tplaybackRate?: number;\n}): () => void {\n\tconst { play } = useSoundEffect(NEW_MESSAGE_SOUND_PATH, {\n\t\tloop: false,\n\t\tvolume: options?.volume ?? 0.7,\n\t\tplaybackRate: options?.playbackRate ?? 1.0,\n\t});\n\n\treturn useCallback(() => {\n\t\tplay();\n\t}, [play]);\n}\n"],"mappings":";;;;AAIA,MAAM,yBAAyB;;;;;;;;;;;;;;;;AAiB/B,SAAgB,mBAAmB,SAGpB;CACd,MAAM,EAAE,SAAS,eAAe,wBAAwB;EACvD,MAAM;EACN,QAAQ,SAAS,UAAU;EAC3B,cAAc,SAAS,gBAAgB;EACvC,CAAC;AAEF,QAAO,kBAAkB;AACxB,QAAM;IACJ,CAAC,KAAK,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/hooks/use-sound-effect.d.ts
|
|
2
|
+
type UseSoundEffectOptions = {
|
|
3
|
+
loop?: boolean;
|
|
4
|
+
volume?: number;
|
|
5
|
+
playbackRate?: number;
|
|
6
|
+
};
|
|
7
|
+
type UseSoundEffectReturn = {
|
|
8
|
+
play: () => void;
|
|
9
|
+
stop: () => void;
|
|
10
|
+
isPlaying: boolean;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Hook to play sound effects using the Web Audio API.
|
|
16
|
+
*
|
|
17
|
+
* @param soundPath - Path to the sound file (relative to public directory or absolute URL)
|
|
18
|
+
* @param options - Configuration options for the sound
|
|
19
|
+
* @returns Object with play, stop functions and state
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const { play, stop, isPlaying } = useSoundEffect('/sounds/notification.wav', {
|
|
23
|
+
* loop: false,
|
|
24
|
+
* volume: 0.5
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
declare function useSoundEffect(soundPath: string, options?: UseSoundEffectOptions): UseSoundEffectReturn;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { UseSoundEffectOptions, UseSoundEffectReturn, useSoundEffect };
|
|
30
|
+
//# sourceMappingURL=use-sound-effect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sound-effect.d.ts","names":[],"sources":["../../src/hooks/use-sound-effect.ts"],"sourcesContent":[],"mappings":";KAEY,qBAAA;EAAA,IAAA,CAAA,EAAA,OAAA;EAMA,MAAA,CAAA,EAAA,MAAA;EAqBI,YAAA,CAAA,EAAA,MAAc;;KArBlB,oBAAA;;;;;SAKJ;;;;;;;;;;;;;;;iBAgBQ,cAAA,8BAEN,wBACP"}
|