@flamingo-stack/openframe-frontend-core 0.0.210 → 0.0.212-snapshot.20260528112413
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VBFOCTMD.cjs → chunk-35XIT2CF.cjs} +17 -17
- package/dist/{chunk-VBFOCTMD.cjs.map → chunk-35XIT2CF.cjs.map} +1 -1
- package/dist/{chunk-ATEUJQKU.js → chunk-3JWIJJ44.js} +2 -2
- package/dist/chunk-CZR7ARBA.js +698 -0
- package/dist/chunk-CZR7ARBA.js.map +1 -0
- package/dist/{chunk-UYQOPC57.js → chunk-HICZPTRR.js} +4 -351
- package/dist/chunk-HICZPTRR.js.map +1 -0
- package/dist/{chunk-WJBPLMBX.js → chunk-IK2X5YJU.js} +3 -3
- package/dist/{chunk-MDTIOPVS.cjs → chunk-OTKJASSX.cjs} +26 -26
- package/dist/{chunk-MDTIOPVS.cjs.map → chunk-OTKJASSX.cjs.map} +1 -1
- package/dist/chunk-OZ3GH6OQ.cjs +698 -0
- package/dist/chunk-OZ3GH6OQ.cjs.map +1 -0
- package/dist/{chunk-6RZYJICV.cjs → chunk-P5EE2VJX.cjs} +1 -1
- package/dist/chunk-P5EE2VJX.cjs.map +1 -0
- package/dist/{chunk-EH3RWVF3.cjs → chunk-WT5JV2GS.cjs} +8 -355
- package/dist/chunk-WT5JV2GS.cjs.map +1 -0
- package/dist/{chunk-TWKPYZNQ.cjs → chunk-ZDF6F7ED.cjs} +569 -694
- package/dist/chunk-ZDF6F7ED.cjs.map +1 -0
- package/dist/{chunk-7L4DWM7P.js → chunk-ZG2YY5E7.js} +1 -1
- package/dist/chunk-ZG2YY5E7.js.map +1 -0
- package/dist/{chunk-R5RNRH62.js → chunk-ZTJVRSN5.js} +422 -547
- package/dist/chunk-ZTJVRSN5.js.map +1 -0
- package/dist/components/chat/hooks/use-chat-identity.d.ts +3 -3
- package/dist/components/chat/hooks/use-chat-identity.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-jetstream-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts +0 -9
- package/dist/components/chat/hooks/use-nats-dialog-subscription.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -5
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +5 -4
- package/dist/components/contact/index.cjs +7 -6
- package/dist/components/contact/index.cjs.map +1 -1
- package/dist/components/contact/index.js +6 -5
- package/dist/components/features/index.cjs +6 -5
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -4
- package/dist/components/features/notifications/index.d.ts +2 -2
- package/dist/components/features/notifications/index.d.ts.map +1 -1
- package/dist/components/features/notifications/notifications-context.d.ts +16 -1
- package/dist/components/features/notifications/notifications-context.d.ts.map +1 -1
- package/dist/components/features/notifications/types.d.ts +4 -0
- package/dist/components/features/notifications/types.d.ts.map +1 -1
- package/dist/components/footer-waitlist-button.d.ts +21 -2
- package/dist/components/footer-waitlist-button.d.ts.map +1 -1
- package/dist/components/index.cjs +93 -96
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +17 -20
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +6 -5
- package/dist/components/navigation/index.cjs.map +1 -1
- package/dist/components/navigation/index.js +5 -4
- package/dist/components/navigation/sticky-section-nav.d.ts.map +1 -1
- package/dist/components/tickets/help-center-card.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +144 -102
- package/dist/components/tickets/index.cjs.map +1 -1
- package/dist/components/tickets/index.js +98 -56
- package/dist/components/tickets/index.js.map +1 -1
- package/dist/components/tickets/ticket-row.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -5
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -4
- package/dist/contexts/chat-runtime-context.d.ts +6 -3
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/embed-shims/index.cjs +3 -3
- package/dist/embed-shims/index.cjs.map +1 -1
- package/dist/embed-shims/index.js +4 -4
- package/dist/hooks/index.cjs +3 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +2 -1
- package/dist/index.cjs +8 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -4
- package/dist/nats/index.cjs +28 -346
- package/dist/nats/index.cjs.map +1 -1
- package/dist/nats/index.d.ts +3 -0
- package/dist/nats/index.d.ts.map +1 -1
- package/dist/nats/index.js +30 -346
- package/dist/nats/index.js.map +1 -1
- package/dist/nats/nats-provider.d.ts +28 -0
- package/dist/nats/nats-provider.d.ts.map +1 -0
- package/dist/nats/nats.d.ts +1 -0
- package/dist/nats/nats.d.ts.map +1 -1
- package/dist/nats/shared-connection.d.ts +73 -0
- package/dist/nats/shared-connection.d.ts.map +1 -0
- package/dist/nats/use-nats-subscription.d.ts +18 -0
- package/dist/nats/use-nats-subscription.d.ts.map +1 -0
- package/dist/utils/index.cjs +10 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +10 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/scroll-into-view.d.ts +63 -0
- package/dist/utils/scroll-into-view.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/chat/hooks/use-chat-identity.ts +8 -7
- package/src/components/chat/hooks/use-jetstream-dialog-subscription.ts +60 -207
- package/src/components/chat/hooks/use-nats-dialog-subscription.ts +71 -214
- package/src/components/features/notifications/index.ts +2 -1
- package/src/components/features/notifications/notifications-context.tsx +104 -6
- package/src/components/features/notifications/types.ts +5 -0
- package/src/components/footer-waitlist-button.tsx +33 -16
- package/src/components/navigation/app-header.tsx +7 -9
- package/src/components/navigation/sticky-section-nav.tsx +6 -4
- package/src/components/tickets/help-center-card.tsx +55 -1
- package/src/components/tickets/help-center-list.tsx +9 -1
- package/src/components/tickets/ticket-detail-drawer.tsx +19 -4
- package/src/components/tickets/ticket-row.tsx +30 -19
- package/src/contexts/chat-runtime-context.tsx +6 -3
- package/src/nats/index.ts +3 -0
- package/src/nats/nats-provider.tsx +146 -0
- package/src/nats/nats.ts +2 -0
- package/src/nats/shared-connection.ts +285 -0
- package/src/nats/use-nats-subscription.ts +99 -0
- package/src/stories/EmbeddableChat.stories.tsx +1 -1
- package/src/utils/index.ts +12 -0
- package/src/utils/scroll-into-view.ts +74 -0
- package/dist/chunk-6RZYJICV.cjs.map +0 -1
- package/dist/chunk-7L4DWM7P.js.map +0 -1
- package/dist/chunk-EH3RWVF3.cjs.map +0 -1
- package/dist/chunk-R5RNRH62.js.map +0 -1
- package/dist/chunk-TWKPYZNQ.cjs.map +0 -1
- package/dist/chunk-UYQOPC57.js.map +0 -1
- /package/dist/{chunk-ATEUJQKU.js.map → chunk-3JWIJJ44.js.map} +0 -0
- /package/dist/{chunk-WJBPLMBX.js.map → chunk-IK2X5YJU.js.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useRouter } from '../embed-shims/next-navigation';
|
|
4
|
+
import { useChatRuntime } from '../contexts/chat-runtime-context';
|
|
4
5
|
import { useCallback } from 'react';
|
|
5
6
|
import { OpenFrameLogo } from './icons';
|
|
6
7
|
import { Button } from './ui/button';
|
|
@@ -11,27 +12,43 @@ export interface FooterWaitlistButtonProps {
|
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Small wrapper around JoinWaitlistButton for use inside the footer.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* Routes through the host's unified-navigation hook
|
|
17
|
+
* (`runtime.navigation.navigate`) when a `ChatRuntimeContext` is
|
|
18
|
+
* mounted — that's the same path EVERY other in-app navigation
|
|
19
|
+
* surface uses (source chips, inline cards, search-autocomplete,
|
|
20
|
+
* action cards). One rule, one decision tree across the whole app.
|
|
21
|
+
* The hub's `HubRuntimeProvider` wires `navigate` to its `useUnifiedNav`
|
|
22
|
+
* helper, so this button picks up cross-platform new-tab decisions,
|
|
23
|
+
* same-URL re-scroll handling, embed-mode short-circuiting, and any
|
|
24
|
+
* future host-side nav rules for free.
|
|
25
|
+
*
|
|
26
|
+
* Falls back to the embed-shim's `router.push` when no runtime is
|
|
27
|
+
* mounted (third-party embedders who haven't set up
|
|
28
|
+
* `ChatRuntimeContext` — the lib stays usable without forcing them
|
|
29
|
+
* to wire the full chat-runtime).
|
|
30
|
+
*
|
|
31
|
+
* Target URL: `/waitlist#top`. `#top` is the canonical "scroll to
|
|
32
|
+
* page top" anchor — the destination page has an explicit
|
|
33
|
+
* `<div id="top">` at the top of `<main>` so native browser anchor
|
|
34
|
+
* scroll works in every browser regardless of the HTML5 magic-anchor
|
|
35
|
+
* behavior.
|
|
16
36
|
*/
|
|
17
37
|
export function FooterWaitlistButton({ className }: FooterWaitlistButtonProps) {
|
|
18
38
|
const router = useRouter();
|
|
19
|
-
const
|
|
39
|
+
const runtime = useChatRuntime();
|
|
20
40
|
|
|
21
41
|
const handleClick = useCallback(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}, 400);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
42
|
+
const href = '/waitlist#top';
|
|
43
|
+
// Prefer the host's unified-nav callback (hub-wired
|
|
44
|
+
// `useUnifiedNav`). Falls back to the embed-shim's router when
|
|
45
|
+
// the host hasn't provided one.
|
|
46
|
+
if (runtime?.navigation?.navigate) {
|
|
47
|
+
const handled = runtime.navigation.navigate({ href });
|
|
48
|
+
if (handled) return;
|
|
32
49
|
}
|
|
33
|
-
router.push(
|
|
34
|
-
}, [
|
|
50
|
+
router.push(href);
|
|
51
|
+
}, [router, runtime]);
|
|
35
52
|
|
|
36
53
|
return (
|
|
37
54
|
<Button
|
|
@@ -6,7 +6,6 @@ import { cn } from '../../utils/cn'
|
|
|
6
6
|
import { useOptionalNotifications } from '../features/notifications/notifications-context'
|
|
7
7
|
import { LogOutIcon, OpenFrameLogo, OpenFrameText, UserIcon } from '../icons'
|
|
8
8
|
import { BellIcon } from '../icons-v2-generated/interface/bell-icon'
|
|
9
|
-
import { BellRingingIcon } from '../icons-v2-generated/interface/bell-ringing-icon'
|
|
10
9
|
import { Menu01Icon, SearchIcon, XmarkIcon } from '../icons-v2-generated'
|
|
11
10
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, SquareAvatar } from '../ui'
|
|
12
11
|
import { HeaderButton } from './header-button'
|
|
@@ -210,20 +209,19 @@ function NotificationsHeaderButton({
|
|
|
210
209
|
dimmedClass,
|
|
211
210
|
}: NotificationsHeaderButtonProps) {
|
|
212
211
|
const ctx = useOptionalNotifications()
|
|
213
|
-
const
|
|
212
|
+
const hasUnread = (ctx?.unreadCount ?? fallbackUnreadCount) > 0
|
|
214
213
|
const isActive = ctx?.isOpen ?? false
|
|
215
214
|
const onClick = ctx?.toggle
|
|
216
|
-
const Icon = unreadCount > 0 ? BellRingingIcon : BellIcon
|
|
217
215
|
|
|
218
216
|
return (
|
|
219
217
|
<HeaderButton
|
|
220
218
|
icon={
|
|
221
|
-
<div className="relative w-
|
|
222
|
-
<
|
|
223
|
-
{
|
|
224
|
-
<span
|
|
225
|
-
|
|
226
|
-
|
|
219
|
+
<div className="relative w-4 h-4 md:w-6 md:h-6">
|
|
220
|
+
<BellIcon className="w-full h-full" />
|
|
221
|
+
{hasUnread && (
|
|
222
|
+
<span
|
|
223
|
+
className="absolute top-0 right-0 bg-ods-warning rounded-full w-1.5 h-1.5 md:w-2 md:h-2"
|
|
224
|
+
/>
|
|
227
225
|
)}
|
|
228
226
|
</div>
|
|
229
227
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useState, useCallback, useRef } from 'react'
|
|
4
4
|
import { cn } from '../../utils'
|
|
5
|
+
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
5
6
|
|
|
6
7
|
export interface StickyNavSection {
|
|
7
8
|
id: string
|
|
@@ -97,7 +98,10 @@ export function useSectionNavigation(
|
|
|
97
98
|
const isScrollingFromClick = useRef(false)
|
|
98
99
|
const { offset = 100 } = options || {}
|
|
99
100
|
|
|
100
|
-
// Handle click -
|
|
101
|
+
// Handle click - scroll to the element via the canonical helper.
|
|
102
|
+
// The `offset` prop maps to `headerOffset` (sticky chrome above the
|
|
103
|
+
// section nav); same smooth-scroll mechanics every other anchor
|
|
104
|
+
// surface in the app uses.
|
|
101
105
|
const handleSectionClick = useCallback((sectionId: string) => {
|
|
102
106
|
const targetElement = document.getElementById(sectionId)
|
|
103
107
|
if (!targetElement) return
|
|
@@ -106,9 +110,7 @@ export function useSectionNavigation(
|
|
|
106
110
|
isScrollingFromClick.current = true
|
|
107
111
|
setActiveSection(sectionId)
|
|
108
112
|
|
|
109
|
-
|
|
110
|
-
const top = targetElement.offsetTop - offset
|
|
111
|
-
window.scrollTo({ top, behavior: 'smooth' })
|
|
113
|
+
scrollElementIntoView(targetElement, { headerOffset: offset })
|
|
112
114
|
|
|
113
115
|
// Allow scroll spy again after scroll completes
|
|
114
116
|
setTimeout(() => {
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* is a SIBLING of the toggle button, not nested inside it.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { useCallback, useRef } from 'react'
|
|
18
19
|
import { StatusBadge, type StatusBadgeProps } from '../ui'
|
|
19
20
|
import { formatRelativeTime } from '../../utils/date-utils'
|
|
21
|
+
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
20
22
|
import { getStatusColorScheme } from '../chat/utils/agent-status-message'
|
|
21
23
|
import { DevCardRowContent } from '../shared/dev-section/dev-card-row'
|
|
22
24
|
import {
|
|
@@ -26,6 +28,23 @@ import {
|
|
|
26
28
|
import type { AnyTicket } from './types'
|
|
27
29
|
import { isOptimistic } from './types'
|
|
28
30
|
|
|
31
|
+
/** Sticky page-chrome offset, applied two ways from this ONE constant:
|
|
32
|
+
*
|
|
33
|
+
* 1. As `scrollMarginTop` inline style on the wrapper — so any
|
|
34
|
+
* anchor-driven or `scrollIntoView()`-driven scroll (browser
|
|
35
|
+
* `#hash` navigation, Tab-focus into the card) lands BELOW the
|
|
36
|
+
* sticky header.
|
|
37
|
+
* 2. As `headerOffset` passed to `scrollElementIntoView(...)` — for
|
|
38
|
+
* the click-to-expand `window.scrollTo` path, which pre-computes
|
|
39
|
+
* its target pixel and ignores CSS `scroll-margin-top`.
|
|
40
|
+
*
|
|
41
|
+
* Single source of truth: change 96 here and BOTH paths follow. The
|
|
42
|
+
* previous code combined a `scroll-mt-24` (=96px) Tailwind class
|
|
43
|
+
* with this constant — two declarations, one comment binding them,
|
|
44
|
+
* drift hazard. Now there's nothing to keep in sync.
|
|
45
|
+
*/
|
|
46
|
+
const STICKY_HEADER_OFFSET_PX = 96
|
|
47
|
+
|
|
29
48
|
export interface HelpCenterCardProps {
|
|
30
49
|
ticket: AnyTicket
|
|
31
50
|
expanded: boolean
|
|
@@ -72,6 +91,39 @@ export function HelpCenterCard({
|
|
|
72
91
|
const isExpandable = !optimistic
|
|
73
92
|
const isExpanded = expanded && isExpandable
|
|
74
93
|
|
|
94
|
+
// Scroll-on-click — delegates to the canonical `scrollElementIntoView`
|
|
95
|
+
// helper with a cross-row layout-shift `adjustTargetY` callback. The
|
|
96
|
+
// helper owns the smooth-scroll mechanics + sticky-chrome offset; we
|
|
97
|
+
// pass the consumer-specific knowledge ("a sibling drawer above me
|
|
98
|
+
// is about to collapse — subtract its height from the target Y").
|
|
99
|
+
//
|
|
100
|
+
// Cross-row gotcha: if ANOTHER row above this one is currently
|
|
101
|
+
// expanded, its drawer collapses simultaneously with our toggle.
|
|
102
|
+
// The collapse shrinks the page above our row → our final Y is
|
|
103
|
+
// HIGHER than the current `rect.top`. By pre-subtracting the
|
|
104
|
+
// collapsing drawer's height we land at the post-shift position
|
|
105
|
+
// cleanly, without scrollIntoView's mid-animation drift.
|
|
106
|
+
const rowRef = useRef<HTMLDivElement | null>(null)
|
|
107
|
+
const handleClick = useCallback(() => {
|
|
108
|
+
onToggle(ticket.id)
|
|
109
|
+
scrollElementIntoView(rowRef.current, {
|
|
110
|
+
headerOffset: STICKY_HEADER_OFFSET_PX,
|
|
111
|
+
adjustTargetY: (raw) => {
|
|
112
|
+
if (!rowRef.current) return raw
|
|
113
|
+
const expandedDrawer = document.querySelector(
|
|
114
|
+
'div[id^="help-center-drawer-"]',
|
|
115
|
+
)
|
|
116
|
+
if (!(expandedDrawer instanceof HTMLElement)) return raw
|
|
117
|
+
const drawerRect = expandedDrawer.getBoundingClientRect()
|
|
118
|
+
const myRect = rowRef.current.getBoundingClientRect()
|
|
119
|
+
// Only adjust when the drawer is ABOVE us. Drawers below us
|
|
120
|
+
// don't shift our position when they collapse.
|
|
121
|
+
if (drawerRect.bottom > myRect.top) return raw
|
|
122
|
+
return raw - drawerRect.height
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}, [onToggle, ticket.id])
|
|
126
|
+
|
|
75
127
|
const rightBadges = (
|
|
76
128
|
<>
|
|
77
129
|
<StatusBadge
|
|
@@ -93,12 +145,14 @@ export function HelpCenterCard({
|
|
|
93
145
|
|
|
94
146
|
return (
|
|
95
147
|
<div
|
|
148
|
+
ref={rowRef}
|
|
149
|
+
style={{ scrollMarginTop: STICKY_HEADER_OFFSET_PX }}
|
|
96
150
|
className={`border-b border-ods-border last:border-b-0 ${optimistic ? 'opacity-60' : ''}`}
|
|
97
151
|
aria-busy={optimistic || undefined}
|
|
98
152
|
>
|
|
99
153
|
<button
|
|
100
154
|
type="button"
|
|
101
|
-
onClick={isExpandable ?
|
|
155
|
+
onClick={isExpandable ? handleClick : undefined}
|
|
102
156
|
disabled={!isExpandable}
|
|
103
157
|
aria-expanded={isExpandable ? isExpanded : undefined}
|
|
104
158
|
aria-controls={isExpanded ? `help-center-drawer-${ticket.id}` : undefined}
|
|
@@ -264,7 +264,15 @@ function HelpCenterListAuthed({
|
|
|
264
264
|
/>
|
|
265
265
|
)
|
|
266
266
|
) : (
|
|
267
|
-
|
|
267
|
+
// `overflow-clip` (NOT `overflow-hidden`) — both visually
|
|
268
|
+
// clip the rounded corners, but `hidden` makes the element
|
|
269
|
+
// a "scroll container" per CSSOM spec, which causes
|
|
270
|
+
// `scrollIntoView` calls inside (`<HelpCenterCard>` click
|
|
271
|
+
// handlers) to try scrolling THIS div (can't, overflow
|
|
272
|
+
// hidden) instead of bubbling up to the window. `clip`
|
|
273
|
+
// keeps the visual clip but NOT the scroll-container
|
|
274
|
+
// status, so click-to-scroll actually moves the page.
|
|
275
|
+
<div className="bg-ods-card border border-ods-border rounded-[6px] overflow-clip w-full">
|
|
268
276
|
{merged.map((ticket) => (
|
|
269
277
|
<HelpCenterCard
|
|
270
278
|
key={ticket.id}
|
|
@@ -212,9 +212,16 @@ function TicketTimelinePanel({ ticket }: { ticket: AnyTicket }) {
|
|
|
212
212
|
return (
|
|
213
213
|
<div className="bg-ods-card border border-ods-border rounded-[6px] overflow-hidden w-full">
|
|
214
214
|
{/* Customer-authored description + any legacy `---`-joined
|
|
215
|
-
comments.
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
comments. Always rendered ABOVE the engagement timeline as
|
|
216
|
+
"Original message" because the server's intake-burst filter
|
|
217
|
+
(see `filterCustomerVisibleTimeline` in
|
|
218
|
+
`hubspot-conversations-utils.ts`) drops the customer's first
|
|
219
|
+
message from engagements when it was part of the HubSpot
|
|
220
|
+
Custom Channel bot intake — bodyTurns IS the canonical
|
|
221
|
+
original for those tickets. For tickets created without bot
|
|
222
|
+
intake (admin-created, email channel) bodyTurns shows the
|
|
223
|
+
manually-entered description and engagements show subsequent
|
|
224
|
+
replies — same flow, no duplication. */}
|
|
218
225
|
{bodyTurns.map((turn, i) => {
|
|
219
226
|
const isResolution = turn.startsWith('[Resolution]')
|
|
220
227
|
const role =
|
|
@@ -315,11 +322,19 @@ function TicketTimelinePanel({ ticket }: { ticket: AnyTicket }) {
|
|
|
315
322
|
avatarSrc = undefined
|
|
316
323
|
}
|
|
317
324
|
|
|
325
|
+
// Role label: every engagement is a customer-visible
|
|
326
|
+
// Conversations message (customer ↔ agent on the Custom
|
|
327
|
+
// Channel). There are no internal Notes on this surface
|
|
328
|
+
// anymore — the read path explicitly filters them. So
|
|
329
|
+
// "Reply" for BOTH sides. The previous "Note" label for
|
|
330
|
+
// support bubbles was a legacy artifact from when Notes
|
|
331
|
+
// were rendered and made customers think their support
|
|
332
|
+
// engineer was leaving internal comments on their ticket.
|
|
318
333
|
return (
|
|
319
334
|
<ConversationCardRow
|
|
320
335
|
key={eng.id}
|
|
321
336
|
author={author}
|
|
322
|
-
role=
|
|
337
|
+
role="Reply"
|
|
323
338
|
avatarSrc={avatarSrc}
|
|
324
339
|
timestamp={eng.createdAt}
|
|
325
340
|
body={stripAttachmentsPreamble(eng.body ?? '')}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* rendered only when this row is the expanded one.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { useCallback, useRef } from 'react'
|
|
22
22
|
import {
|
|
23
23
|
Collapsible,
|
|
24
24
|
CollapsibleContent,
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ChatTicketItemData,
|
|
29
29
|
} from '../chat/entity-cards/chat-ticket-item'
|
|
30
30
|
import { formatRelativeTime } from '../../utils/date-utils'
|
|
31
|
+
import { scrollElementIntoView } from '../../utils/scroll-into-view'
|
|
31
32
|
import {
|
|
32
33
|
TicketDetailDrawer,
|
|
33
34
|
type TicketDetailDrawerProps,
|
|
@@ -64,25 +65,35 @@ export function TicketRow({
|
|
|
64
65
|
// arrived yet, so action targets would be undefined.
|
|
65
66
|
const optimistic = isOptimistic(ticket)
|
|
66
67
|
|
|
67
|
-
// Scroll the
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// hidden — the user has to scroll manually to see what they just
|
|
71
|
-
// opened.
|
|
68
|
+
// Scroll the clicked card to the top of the viewport. Every click
|
|
69
|
+
// scrolls — first-click expansion, same-row re-click, cross-row
|
|
70
|
+
// switch. The clicked card lands at the top.
|
|
72
71
|
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
72
|
+
// Cross-row gotcha: if ANOTHER row above this one is currently
|
|
73
|
+
// expanded, its drawer is about to collapse simultaneously with our
|
|
74
|
+
// toggle. We pre-subtract its height from the target Y so the
|
|
75
|
+
// smooth-scroll lands at the FINAL post-collapse position cleanly.
|
|
76
|
+
// Same pattern as `<HelpCenterCard>` — the only diff is the drawer
|
|
77
|
+
// id prefix (`ticket-drawer-` vs `help-center-drawer-`).
|
|
77
78
|
const rowRef = useRef<HTMLDivElement | null>(null)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
rowRef.current
|
|
83
|
-
|
|
79
|
+
const handleClick = useCallback(() => {
|
|
80
|
+
onToggle(ticket.id)
|
|
81
|
+
scrollElementIntoView(rowRef.current, {
|
|
82
|
+
adjustTargetY: (raw) => {
|
|
83
|
+
if (!rowRef.current) return raw
|
|
84
|
+
const expandedDrawer = document.querySelector(
|
|
85
|
+
'div[id^="ticket-drawer-"]',
|
|
86
|
+
)
|
|
87
|
+
if (!(expandedDrawer instanceof HTMLElement)) return raw
|
|
88
|
+
const drawerRect = expandedDrawer.getBoundingClientRect()
|
|
89
|
+
const myRect = rowRef.current.getBoundingClientRect()
|
|
90
|
+
// Only adjust when the drawer is ABOVE us. Drawers below
|
|
91
|
+
// don't shift our position when they collapse.
|
|
92
|
+
if (drawerRect.bottom > myRect.top) return raw
|
|
93
|
+
return raw - drawerRect.height
|
|
94
|
+
},
|
|
84
95
|
})
|
|
85
|
-
}, [
|
|
96
|
+
}, [onToggle, ticket.id])
|
|
86
97
|
|
|
87
98
|
const tileData: ChatTicketItemData = {
|
|
88
99
|
id: ticket.id,
|
|
@@ -112,14 +123,14 @@ export function TicketRow({
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
return (
|
|
115
|
-
<div ref={rowRef} className="scroll-mt-
|
|
126
|
+
<div ref={rowRef} className="scroll-mt-24">
|
|
116
127
|
<Collapsible
|
|
117
128
|
open={expanded && !optimistic}
|
|
118
129
|
className="border-b border-ods-border last:border-b-0"
|
|
119
130
|
>
|
|
120
131
|
<ChatTicketItem
|
|
121
132
|
ticket={tileData}
|
|
122
|
-
onClick={optimistic ? undefined :
|
|
133
|
+
onClick={optimistic ? undefined : handleClick}
|
|
123
134
|
aria-expanded={expanded && !optimistic}
|
|
124
135
|
aria-controls={`ticket-drawer-${ticket.id}`}
|
|
125
136
|
/>
|
|
@@ -62,12 +62,15 @@ export interface ChatRuntime {
|
|
|
62
62
|
* relative `/api/storage/view/chat-attachments/` is sufficient
|
|
63
63
|
* (same-origin); embedders supply an absolute hub URL so the
|
|
64
64
|
* browser can fetch cross-origin.
|
|
65
|
-
* - `
|
|
65
|
+
* - `identityUrl` — GET endpoint the `useChatIdentity` hook
|
|
66
66
|
* hits to learn the `{authTier, source, attachmentsEnabled}`
|
|
67
|
-
* capability bag for the current session.
|
|
67
|
+
* capability bag for the current session. Used beyond chat
|
|
68
|
+
* (tickets / contact form / any embedded surface that needs
|
|
69
|
+
* to identify the proxied customer), so the name has no
|
|
70
|
+
* "chat" prefix even though the consuming hook still does. */
|
|
68
71
|
attachmentUploadUrl: string
|
|
69
72
|
attachmentViewUrlPrefix: string
|
|
70
|
-
|
|
73
|
+
identityUrl: string
|
|
71
74
|
/** Optional URL prefix for the image proxy (`<prefix>?url=<external>`).
|
|
72
75
|
* When unset, lib's `getProxiedImageUrl` returns the original URL
|
|
73
76
|
* unchanged. Hub default: '/api/image-proxy'. Embedders that don't
|
package/src/nats/index.ts
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import type { NatsClient, NatsStatus } from './nats'
|
|
5
|
+
import {
|
|
6
|
+
acquireClient,
|
|
7
|
+
releaseClient,
|
|
8
|
+
startConnectionLifecycle,
|
|
9
|
+
type AcquireClientOptions,
|
|
10
|
+
type NatsReconnectionBackoff,
|
|
11
|
+
} from './shared-connection'
|
|
12
|
+
|
|
13
|
+
export type { NatsReconnectionBackoff } from './shared-connection'
|
|
14
|
+
|
|
15
|
+
export interface NatsProviderProps {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
/** Return the current NATS WebSocket URL (or null when not yet available, e.g. unauthenticated). */
|
|
18
|
+
getWsUrl: () => string | null
|
|
19
|
+
/** Called before each reconnect attempt. */
|
|
20
|
+
onBeforeReconnect?: () => Promise<void> | void
|
|
21
|
+
clientConfig?: AcquireClientOptions
|
|
22
|
+
reconnectionBackoff?: NatsReconnectionBackoff
|
|
23
|
+
/**
|
|
24
|
+
* Bump this to force re-evaluating `getWsUrl()` (e.g. when auth state flips).
|
|
25
|
+
* Provider does not subscribe to external auth state by itself.
|
|
26
|
+
*/
|
|
27
|
+
urlRevision?: unknown
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NatsContextValue {
|
|
31
|
+
client: NatsClient | null
|
|
32
|
+
status: NatsStatus
|
|
33
|
+
isReady: boolean
|
|
34
|
+
reconnectionCount: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NatsContext = React.createContext<NatsContextValue | null>(null)
|
|
38
|
+
|
|
39
|
+
export function NatsProvider({
|
|
40
|
+
children,
|
|
41
|
+
getWsUrl,
|
|
42
|
+
onBeforeReconnect,
|
|
43
|
+
clientConfig,
|
|
44
|
+
reconnectionBackoff,
|
|
45
|
+
urlRevision,
|
|
46
|
+
}: NatsProviderProps) {
|
|
47
|
+
const [client, setClient] = React.useState<NatsClient | null>(null)
|
|
48
|
+
const [status, setStatus] = React.useState<NatsStatus>('closed')
|
|
49
|
+
const [reconnectionCount, setReconnectionCount] = React.useState(0)
|
|
50
|
+
|
|
51
|
+
const getWsUrlRef = React.useRef(getWsUrl)
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
getWsUrlRef.current = getWsUrl
|
|
54
|
+
}, [getWsUrl])
|
|
55
|
+
|
|
56
|
+
const onBeforeReconnectRef = React.useRef(onBeforeReconnect)
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
onBeforeReconnectRef.current = onBeforeReconnect
|
|
59
|
+
}, [onBeforeReconnect])
|
|
60
|
+
|
|
61
|
+
const reconnectionBackoffRef = React.useRef(reconnectionBackoff)
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
reconnectionBackoffRef.current = reconnectionBackoff
|
|
64
|
+
}, [reconnectionBackoff])
|
|
65
|
+
|
|
66
|
+
const clientConfigRef = React.useRef(clientConfig)
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
clientConfigRef.current = clientConfig
|
|
69
|
+
}, [clientConfig])
|
|
70
|
+
|
|
71
|
+
const heldUrlRef = React.useRef<string | null>(null)
|
|
72
|
+
const hadConnectionBeforeRef = React.useRef(false)
|
|
73
|
+
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
const wsUrl = getWsUrlRef.current()
|
|
76
|
+
|
|
77
|
+
if (!wsUrl) {
|
|
78
|
+
if (heldUrlRef.current) {
|
|
79
|
+
releaseClient(heldUrlRef.current)
|
|
80
|
+
heldUrlRef.current = null
|
|
81
|
+
setClient(null)
|
|
82
|
+
setStatus('closed')
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (heldUrlRef.current && heldUrlRef.current !== wsUrl) {
|
|
88
|
+
releaseClient(heldUrlRef.current)
|
|
89
|
+
heldUrlRef.current = null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const conn = acquireClient(wsUrl, clientConfigRef.current)
|
|
93
|
+
heldUrlRef.current = wsUrl
|
|
94
|
+
setClient(conn.client)
|
|
95
|
+
setStatus(conn.client.isConnected() ? 'connected' : 'connecting')
|
|
96
|
+
|
|
97
|
+
const lifecycle = startConnectionLifecycle({
|
|
98
|
+
conn,
|
|
99
|
+
wsUrl,
|
|
100
|
+
onBeforeReconnect: () => onBeforeReconnectRef.current?.(),
|
|
101
|
+
backoff: reconnectionBackoffRef.current,
|
|
102
|
+
getFreshUrl: () => getWsUrlRef.current(),
|
|
103
|
+
onStatusChange: (newStatus) => {
|
|
104
|
+
setStatus(newStatus)
|
|
105
|
+
if (newStatus === 'connected') {
|
|
106
|
+
if (hadConnectionBeforeRef.current) {
|
|
107
|
+
setReconnectionCount((c) => c + 1)
|
|
108
|
+
}
|
|
109
|
+
hadConnectionBeforeRef.current = true
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
lifecycle.stop()
|
|
116
|
+
if (heldUrlRef.current) {
|
|
117
|
+
releaseClient(heldUrlRef.current)
|
|
118
|
+
heldUrlRef.current = null
|
|
119
|
+
}
|
|
120
|
+
setClient(null)
|
|
121
|
+
setStatus('closed')
|
|
122
|
+
}
|
|
123
|
+
}, [urlRevision])
|
|
124
|
+
|
|
125
|
+
const value = React.useMemo<NatsContextValue>(
|
|
126
|
+
() => ({
|
|
127
|
+
client,
|
|
128
|
+
status,
|
|
129
|
+
isReady: status === 'connected' && client !== null,
|
|
130
|
+
reconnectionCount,
|
|
131
|
+
}),
|
|
132
|
+
[client, status, reconnectionCount],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return <NatsContext.Provider value={value}>{children}</NatsContext.Provider>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function useNats(): NatsContextValue {
|
|
139
|
+
const ctx = React.useContext(NatsContext)
|
|
140
|
+
if (!ctx) throw new Error('useNats must be used inside <NatsProvider>')
|
|
141
|
+
return ctx
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function useOptionalNats(): NatsContextValue | null {
|
|
145
|
+
return React.useContext(NatsContext)
|
|
146
|
+
}
|
package/src/nats/nats.ts
CHANGED