app-tutor-ai-consumer 1.18.2 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/public/assets/svg/error-dark.svg +27 -0
- package/public/assets/svg/error-light.svg +27 -0
- package/src/config/tests/handlers.ts +12 -0
- package/src/development-bootstrap.tsx +5 -2
- package/src/index.tsx +3 -0
- package/src/lib/components/button/button.tsx +105 -14
- package/src/lib/components/button/styles.module.css +9 -0
- package/src/lib/components/errors/generic/generic-error.tsx +58 -3
- package/src/lib/components/icons/arrow-up.svg +5 -0
- package/src/lib/components/icons/copy.svg +5 -0
- package/src/lib/components/icons/icon-names.d.ts +3 -0
- package/src/lib/components/icons/like.svg +5 -0
- package/src/modules/messages/components/message-actions/index.ts +2 -0
- package/src/modules/messages/components/message-actions/message-actions.tsx +49 -0
- package/src/modules/messages/components/message-item/message-item.tsx +21 -5
- package/src/modules/messages/components/message-item-error/message-item-error.tsx +16 -9
- package/src/modules/messages/components/message-skeleton/message-skeleton.tsx +1 -4
- package/src/modules/messages/components/messages-container/index.ts +2 -0
- package/src/modules/messages/components/messages-container/messages-container.tsx +91 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +9 -82
- package/src/modules/messages/constants.ts +5 -0
- package/src/modules/messages/events.ts +12 -4
- package/src/modules/messages/hooks/index.ts +1 -0
- package/src/modules/messages/hooks/use-all-messages/use-all-messages.tsx +1 -2
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx +18 -19
- package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.tsx +41 -35
- package/src/modules/messages/hooks/use-scroller/index.ts +2 -0
- package/src/modules/messages/hooks/use-scroller/use-scroller.tsx +50 -0
- package/src/modules/messages/hooks/use-send-text-message/use-send-text-message.tsx +31 -2
- package/src/modules/messages/hooks/use-subscribe-message-received-event/use-subscribe-message-received-event.tsx +47 -64
- package/src/modules/messages/store/index.ts +1 -0
- package/src/modules/messages/store/messages-max-count.atom.ts +13 -0
- package/src/modules/messages/utils/index.ts +2 -0
- package/src/modules/messages/utils/set-messages-cache/index.ts +1 -0
- package/src/modules/messages/utils/set-messages-cache/utils.ts +53 -0
- package/src/modules/widget/components/chat-page/chat-page.spec.tsx +23 -7
- package/src/modules/widget/components/chat-page/chat-page.tsx +70 -14
- package/src/modules/widget/components/greetings-card/greetings-card.tsx +1 -1
- package/src/modules/widget/components/header/header.tsx +6 -4
- package/src/modules/widget/components/starter-page/starter-page.spec.tsx +4 -1
- package/src/modules/widget/components/starter-page/starter-page.tsx +31 -5
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
import type { MouseEventHandler, PropsWithChildren } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
|
|
7
|
+
import { Button, Icon } from '@/src/lib/components'
|
|
8
|
+
import { MessageSkeleton } from '@/src/modules/messages/components'
|
|
9
|
+
import { useSkeletonRef } from '@/src/modules/messages/hooks/use-skeleton-ref'
|
|
10
|
+
import {
|
|
11
|
+
ScrollToBottomButton,
|
|
12
|
+
usePageLayoutMainRefContext,
|
|
13
|
+
useWidgetLoadingAtom
|
|
14
|
+
} from '@/src/modules/widget'
|
|
15
|
+
import { useScroller } from '../../hooks'
|
|
16
|
+
|
|
17
|
+
const MessagesContainer = forwardRef<
|
|
18
|
+
HTMLDivElement,
|
|
19
|
+
PropsWithChildren<{
|
|
20
|
+
showButton?: boolean
|
|
21
|
+
loading?: boolean
|
|
22
|
+
handleShowMore?: () => Promise<void>
|
|
23
|
+
}>
|
|
24
|
+
>(({ children, handleShowMore, showButton = false, loading = false }, forwardedRef) => {
|
|
25
|
+
const { t } = useTranslation()
|
|
26
|
+
const skeletonRef = useSkeletonRef()
|
|
27
|
+
const [isLoadingNewMsg] = useWidgetLoadingAtom()
|
|
28
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
29
|
+
const { scrollerRef, scrollToButtonRef, scrollToBottom, showScrollButton } =
|
|
30
|
+
useScroller(forwardedRef)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
scrollToBottom()
|
|
34
|
+
}, [scrollToBottom])
|
|
35
|
+
|
|
36
|
+
const handleClickShowMore: MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
37
|
+
const scroller = scrollerRef?.current
|
|
38
|
+
const heightBeforeRender = Number(e?.currentTarget?.scrollHeight)
|
|
39
|
+
|
|
40
|
+
void handleShowMore?.().then(() => {
|
|
41
|
+
if (scroller && !isNaN(heightBeforeRender)) {
|
|
42
|
+
setTimeout(
|
|
43
|
+
() =>
|
|
44
|
+
scroller.scrollTo({
|
|
45
|
+
top: heightBeforeRender + 10,
|
|
46
|
+
behavior: 'smooth'
|
|
47
|
+
}),
|
|
48
|
+
180
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div ref={scrollerRef} className='mx-2 my-4 flex h-full flex-col gap-2 overflow-auto px-4'>
|
|
56
|
+
<div className='mb-auto flex-1 self-center'>
|
|
57
|
+
<Button
|
|
58
|
+
className='max-w-max rounded-full border border-neutral-300 bg-neutral-200 px-2 py-1 text-xs/normal tracking-wide text-neutral-900'
|
|
59
|
+
onClick={handleClickShowMore}
|
|
60
|
+
loading={loading}
|
|
61
|
+
show={showButton}>
|
|
62
|
+
<Icon name='arrow-up' className='h-4 w-3' aria-hidden />
|
|
63
|
+
<span className='text-nowrap'>{t('general.buttons.show_more')}</span>
|
|
64
|
+
</Button>
|
|
65
|
+
</div>
|
|
66
|
+
{children}
|
|
67
|
+
|
|
68
|
+
{mainLayoutRef.current &&
|
|
69
|
+
createPortal(
|
|
70
|
+
<ScrollToBottomButton
|
|
71
|
+
ref={scrollToButtonRef}
|
|
72
|
+
show={showScrollButton}
|
|
73
|
+
onClick={scrollToBottom}
|
|
74
|
+
/>,
|
|
75
|
+
mainLayoutRef.current
|
|
76
|
+
)}
|
|
77
|
+
<div
|
|
78
|
+
className={clsx({
|
|
79
|
+
'pointer-events-none h-0 overflow-hidden opacity-0': !isLoadingNewMsg,
|
|
80
|
+
'mt-2 pb-4': isLoadingNewMsg
|
|
81
|
+
})}
|
|
82
|
+
ref={skeletonRef}>
|
|
83
|
+
<MessageSkeleton />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
MessagesContainer.displayName = 'MessagesContainer'
|
|
90
|
+
|
|
91
|
+
export default MessagesContainer
|
|
@@ -1,89 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { createPortal } from 'react-dom'
|
|
1
|
+
import type { ParsedMessage } from '@/src/modules/messages'
|
|
2
|
+
import { MessageItem } from '@/src/modules/messages/components'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { useSkeletonRef } from '../../hooks/use-skeleton-ref'
|
|
8
|
-
import { MessageItem } from '../message-item'
|
|
9
|
-
import { MessageSkeleton } from '../message-skeleton'
|
|
4
|
+
function MessagesList({ messagesMap }: { messagesMap: Map<string, ParsedMessage[]> }) {
|
|
5
|
+
if (!(messagesMap.size > 0)) return null
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const MessageItemEndOfScroll = lazy(
|
|
16
|
-
() => import('../message-item-end-of-scroll/message-item-end-of-scroll')
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
const ScrollToBottomButton = lazy(
|
|
20
|
-
() => import('@/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button')
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
function MessagesList() {
|
|
24
|
-
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
25
|
-
const scrollToButtonRef = useRef<HTMLButtonElement>(null)
|
|
26
|
-
const { allMessages, messagesQuery } = useAllMessages()
|
|
27
|
-
const widgetIsLoading = useWidgetLoadingAtomValue()
|
|
28
|
-
const skeletonRef = useSkeletonRef()
|
|
29
|
-
const { showScrollButton } = useManageScroll(scrollerRef)
|
|
30
|
-
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
31
|
-
|
|
32
|
-
const scrollToBottom = useCallback(() => {
|
|
33
|
-
const { current: scroller } = scrollerRef
|
|
34
|
-
|
|
35
|
-
if (!scroller) return
|
|
36
|
-
|
|
37
|
-
scroller.scrollTo({
|
|
38
|
-
top: scroller.scrollHeight,
|
|
39
|
-
behavior: 'smooth'
|
|
40
|
-
})
|
|
41
|
-
}, [])
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div ref={scrollerRef} className='mx-2 my-4 flex flex-col gap-2 overflow-auto px-4'>
|
|
45
|
-
<MessageItemLoading show={messagesQuery.isFetching} />
|
|
46
|
-
|
|
47
|
-
<MessageItemEndOfScroll
|
|
48
|
-
show={!messagesQuery.isFetching && !messagesQuery.hasNextPage && allMessages.length > 0}
|
|
49
|
-
/>
|
|
50
|
-
{mainLayoutRef.current &&
|
|
51
|
-
createPortal(
|
|
52
|
-
<ScrollToBottomButton
|
|
53
|
-
ref={scrollToButtonRef}
|
|
54
|
-
show={showScrollButton}
|
|
55
|
-
onClick={scrollToBottom}
|
|
56
|
-
/>,
|
|
57
|
-
mainLayoutRef.current
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
{allMessages?.map(([publishingDate, messages], i) => (
|
|
61
|
-
<div key={i} className='flex flex-1 flex-col justify-center gap-6'>
|
|
62
|
-
<span className='self-center rounded-full border border-neutral-300 bg-neutral-200 px-4 py-2 text-xs capitalize text-neutral-900'>
|
|
63
|
-
{publishingDate}
|
|
64
|
-
</span>
|
|
65
|
-
{messages.map((msg, k) => (
|
|
66
|
-
<MessageItem key={`${msg.id}-${k}`} message={msg} />
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
7
|
+
return Array.from(messagesMap).map(([, messages], i) => (
|
|
8
|
+
<div key={i} className='flex flex-1 flex-col justify-center gap-6'>
|
|
9
|
+
{messages.map((msg, k) => (
|
|
10
|
+
<MessageItem key={`${msg.id}-${k}`} message={msg} />
|
|
69
11
|
))}
|
|
70
|
-
|
|
71
|
-
<MessageItemError
|
|
72
|
-
show={Boolean(messagesQuery.error)}
|
|
73
|
-
message={`❌ Error loading messages: ${messagesQuery.error?.message ?? ''}`}
|
|
74
|
-
retry={() => void messagesQuery.refetch()}
|
|
75
|
-
/>
|
|
76
|
-
|
|
77
|
-
<div
|
|
78
|
-
className={clsx({
|
|
79
|
-
'pointer-events-none h-0 overflow-hidden opacity-0': !widgetIsLoading,
|
|
80
|
-
'pb-4': widgetIsLoading
|
|
81
|
-
})}
|
|
82
|
-
ref={skeletonRef}>
|
|
83
|
-
<MessageSkeleton />
|
|
84
|
-
</div>
|
|
85
12
|
</div>
|
|
86
|
-
)
|
|
13
|
+
))
|
|
87
14
|
}
|
|
88
15
|
|
|
89
16
|
export default MessagesList
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import type { ICustomEvent } from '@/src/types'
|
|
2
|
-
|
|
3
1
|
import type { SubmitQuestionEventDetail } from './types'
|
|
4
2
|
|
|
5
3
|
export const MessagesEventTypes = {
|
|
6
4
|
SUBMIT_QUESTION: 'c3po-chat:questionSubmitted'
|
|
7
5
|
} as const
|
|
8
6
|
|
|
9
|
-
const MessagesEventsList
|
|
7
|
+
const MessagesEventsList = [
|
|
10
8
|
{
|
|
11
9
|
name: MessagesEventTypes.SUBMIT_QUESTION,
|
|
12
|
-
handler: () =>
|
|
10
|
+
handler: (callback: () => void) => {
|
|
11
|
+
const listener = () => {
|
|
12
|
+
callback()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
window.addEventListener(MessagesEventTypes.SUBMIT_QUESTION, listener)
|
|
16
|
+
|
|
17
|
+
return () => {
|
|
18
|
+
window.removeEventListener(MessagesEventTypes.SUBMIT_QUESTION, listener)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
13
21
|
dispatch: () => {
|
|
14
22
|
const event: CustomEventInit<SubmitQuestionEventDetail> = {
|
|
15
23
|
detail: {
|
|
@@ -2,5 +2,6 @@ export * from './use-all-messages'
|
|
|
2
2
|
export * from './use-fetch-messages'
|
|
3
3
|
export * from './use-infinite-get-messages'
|
|
4
4
|
export * from './use-manage-scroll'
|
|
5
|
+
export * from './use-scroller'
|
|
5
6
|
export * from './use-send-text-message'
|
|
6
7
|
export * from './use-subscribe-message-received-event'
|
|
@@ -10,8 +10,7 @@ function useAllMessages() {
|
|
|
10
10
|
|
|
11
11
|
const messagesQuery = useInfiniteGetMessages({
|
|
12
12
|
conversationId: settings?.conversationId ?? '',
|
|
13
|
-
profileId: profileQuery.data?.id ?? ''
|
|
14
|
-
enabled: Boolean(settings?.conversationId && profileQuery.data?.id)
|
|
13
|
+
profileId: profileQuery.data?.id ?? ''
|
|
15
14
|
})
|
|
16
15
|
|
|
17
16
|
const allMessages = useMemo(
|
package/src/modules/messages/hooks/use-infinite-get-messages/use-infinite-get-messages.spec.tsx
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { act, renderHook, waitFor } from '@/src/config/tests'
|
|
4
|
-
import { SparkieService } from '@/src/modules/sparkie'
|
|
5
|
-
import { SparkieMessageServiceMock } from '@/src/modules/sparkie/__tests__/sparkie.mock'
|
|
1
|
+
import { act, MockRequest, renderHook, waitFor } from '@/src/config/tests'
|
|
6
2
|
import IMessageWithSenderDataMock from '../../__tests__/imessage-with-sender-data.mock'
|
|
3
|
+
import { MessagesEndpoints, MSG_MAX_COUNT } from '../../constants'
|
|
7
4
|
import type { IMessageWithSenderData } from '../../types'
|
|
8
5
|
|
|
9
6
|
import useInfiniteGetMessages from './use-infinite-get-messages'
|
|
@@ -11,21 +8,16 @@ import useInfiniteGetMessages from './use-infinite-get-messages'
|
|
|
11
8
|
describe('useInfiniteGetMessages', () => {
|
|
12
9
|
const mockConversationId = 'conversation-123'
|
|
13
10
|
const mockProfileId = 'profile-456'
|
|
14
|
-
const
|
|
11
|
+
const limitMock = 2
|
|
15
12
|
|
|
16
13
|
beforeEach(() => {
|
|
17
14
|
vi.clearAllMocks()
|
|
18
|
-
vi.spyOn(SparkieService, 'getMessageService').mockResolvedValue(
|
|
19
|
-
SparkieMessageServiceMock as unknown as MessageService
|
|
20
|
-
)
|
|
21
|
-
SparkieMessageServiceMock.getAll.mockClear()
|
|
22
15
|
})
|
|
23
16
|
|
|
24
17
|
const createHook = (
|
|
25
18
|
props = {
|
|
26
19
|
conversationId: mockConversationId,
|
|
27
|
-
profileId: mockProfileId
|
|
28
|
-
enabled: mockEnabled
|
|
20
|
+
profileId: mockProfileId
|
|
29
21
|
}
|
|
30
22
|
) => renderHook(() => useInfiniteGetMessages(props))
|
|
31
23
|
|
|
@@ -34,22 +26,28 @@ describe('useInfiniteGetMessages', () => {
|
|
|
34
26
|
|
|
35
27
|
await waitFor(() => expect(result.current.data).toBeDefined())
|
|
36
28
|
|
|
37
|
-
expect(result.current.data?.size).toBe(
|
|
29
|
+
expect(result.current.data?.size).toBe(MSG_MAX_COUNT)
|
|
38
30
|
})
|
|
39
31
|
|
|
40
32
|
it('should be able to fetch next pages', async () => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
const allMessages = limitMock + 1
|
|
34
|
+
|
|
35
|
+
MockRequest.mock(
|
|
36
|
+
MessagesEndpoints.getAll(mockConversationId),
|
|
37
|
+
new IMessageWithSenderDataMock().getMany(allMessages) as IMessageWithSenderData[]
|
|
44
38
|
)
|
|
45
39
|
|
|
46
|
-
const { result } = createHook(
|
|
40
|
+
const { result } = createHook({
|
|
41
|
+
conversationId: mockConversationId,
|
|
42
|
+
profileId: mockProfileId,
|
|
43
|
+
limit: limitMock
|
|
44
|
+
} as never)
|
|
47
45
|
|
|
48
46
|
await waitFor(() => {
|
|
49
47
|
expect(result.current.isSuccess).toBe(true)
|
|
50
48
|
})
|
|
51
49
|
|
|
52
|
-
expect(result.current.data?.size).toBe(
|
|
50
|
+
expect(result.current.data?.size).toBe(allMessages)
|
|
53
51
|
|
|
54
52
|
act(() => {
|
|
55
53
|
void result.current.fetchNextPage()
|
|
@@ -59,7 +57,8 @@ describe('useInfiniteGetMessages', () => {
|
|
|
59
57
|
expect(result.current.fetchStatus).toBe('idle')
|
|
60
58
|
})
|
|
61
59
|
|
|
62
|
-
expect(result.current.data?.size).toBe(
|
|
60
|
+
expect(result.current.data?.size).toBe(allMessages)
|
|
61
|
+
|
|
63
62
|
expect(result.current.hasNextPage).toBe(false)
|
|
64
63
|
})
|
|
65
64
|
})
|
|
@@ -2,31 +2,38 @@ import type { UndefinedInitialDataInfiniteOptions } from '@tanstack/react-query'
|
|
|
2
2
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
3
3
|
|
|
4
4
|
import { formatTime } from '@/src/config/dayjs'
|
|
5
|
-
import {
|
|
6
|
-
import { MSG_MAX_COUNT } from '../../constants'
|
|
7
|
-
import type { FetchMessagesResponse, ParsedMessage } from '../../types'
|
|
5
|
+
import { api } from '@/src/config/request'
|
|
6
|
+
import { MessagesEndpoints, MSG_MAX_COUNT } from '../../constants'
|
|
7
|
+
import type { FetchMessagesResponse, IMessageWithSenderData, ParsedMessage } from '../../types'
|
|
8
8
|
import { messagesParser } from '../../utils'
|
|
9
|
-
import type { UseFetchMessagesProps } from '../use-fetch-messages'
|
|
10
9
|
|
|
11
|
-
export type
|
|
12
|
-
conversationId
|
|
13
|
-
profileId
|
|
14
|
-
|
|
10
|
+
export type GetAllMessagesQueryProps = {
|
|
11
|
+
conversationId?: string
|
|
12
|
+
profileId?: string
|
|
13
|
+
limit?: number
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
export const
|
|
16
|
+
export const getAllMessagesQuery = ({
|
|
18
17
|
conversationId,
|
|
19
18
|
profileId,
|
|
20
|
-
|
|
21
|
-
}:
|
|
19
|
+
limit = MSG_MAX_COUNT
|
|
20
|
+
}: GetAllMessagesQueryProps) =>
|
|
22
21
|
({
|
|
23
|
-
queryKey: ['
|
|
22
|
+
queryKey: ['getAllMessagesWithoutSparkie', limit, conversationId],
|
|
24
23
|
queryFn: async ({ pageParam }: { pageParam?: number }) => {
|
|
25
|
-
const messages = await
|
|
24
|
+
const { data: messages } = await api.get<IMessageWithSenderData[]>(
|
|
25
|
+
MessagesEndpoints.getAll(conversationId as string),
|
|
26
|
+
{
|
|
27
|
+
params: {
|
|
28
|
+
before: pageParam,
|
|
29
|
+
limit
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
)
|
|
26
33
|
|
|
27
34
|
return {
|
|
28
35
|
messages,
|
|
29
|
-
hasMore: messages.length ===
|
|
36
|
+
hasMore: messages.length === limit
|
|
30
37
|
} as FetchMessagesResponse
|
|
31
38
|
},
|
|
32
39
|
initialPageParam: undefined,
|
|
@@ -37,26 +44,29 @@ export const getMessagesInfiniteQuery = ({
|
|
|
37
44
|
|
|
38
45
|
return minSentAt
|
|
39
46
|
},
|
|
40
|
-
enabled,
|
|
47
|
+
enabled: Boolean(conversationId && profileId) && limit > 0,
|
|
41
48
|
select(data) {
|
|
42
49
|
const messages = data.pages?.flatMap((page) => page.messages) ?? []
|
|
43
|
-
return messagesParser({ messages, profileId }).reduce(
|
|
44
|
-
|
|
50
|
+
return messagesParser({ messages, profileId: profileId as string }).reduce(
|
|
51
|
+
(messagesMap, currentMessage) => {
|
|
52
|
+
const timestamp = formatTime(currentMessage.timestamp, true)
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
if (!messagesMap.has(timestamp)) {
|
|
55
|
+
messagesMap.set(timestamp, [currentMessage])
|
|
56
|
+
return messagesMap
|
|
57
|
+
}
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
const existingTimestampValues = Array.from(messagesMap.get(timestamp) ?? [])
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
messagesMap.set(
|
|
62
|
+
timestamp,
|
|
63
|
+
[...existingTimestampValues, currentMessage].sort((a, b) => a.timestamp - b.timestamp)
|
|
64
|
+
)
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
return messagesMap
|
|
67
|
+
},
|
|
68
|
+
new Map<string, ParsedMessage[]>()
|
|
69
|
+
)
|
|
60
70
|
}
|
|
61
71
|
}) as UndefinedInitialDataInfiniteOptions<
|
|
62
72
|
FetchMessagesResponse,
|
|
@@ -64,15 +74,11 @@ export const getMessagesInfiniteQuery = ({
|
|
|
64
74
|
Map<string, ParsedMessage[]>
|
|
65
75
|
>
|
|
66
76
|
|
|
67
|
-
function useInfiniteGetMessages({
|
|
68
|
-
|
|
69
|
-
profileId,
|
|
70
|
-
enabled = false
|
|
71
|
-
}: Omit<UseFetchMessagesProps, 'currentMessages' | 'loadFirstPage'>) {
|
|
72
|
-
const query = getMessagesInfiniteQuery({
|
|
77
|
+
function useInfiniteGetMessages({ conversationId, profileId, limit }: GetAllMessagesQueryProps) {
|
|
78
|
+
const query = getAllMessagesQuery({
|
|
73
79
|
conversationId,
|
|
74
80
|
profileId,
|
|
75
|
-
|
|
81
|
+
limit
|
|
76
82
|
})
|
|
77
83
|
|
|
78
84
|
return useInfiniteQuery(query)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
|
2
|
+
import type { ForwardedRef } from 'react'
|
|
3
|
+
|
|
4
|
+
const threshold = 50
|
|
5
|
+
|
|
6
|
+
function useScroller(forwardedRef: ForwardedRef<HTMLDivElement>) {
|
|
7
|
+
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
8
|
+
const scrollToButtonRef = useRef<HTMLButtonElement>(null)
|
|
9
|
+
const [showScrollButton, setShowScrollButton] = useState(false)
|
|
10
|
+
|
|
11
|
+
useImperativeHandle(forwardedRef, () => scrollerRef?.current as HTMLDivElement)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const { current: scroller } = scrollerRef
|
|
15
|
+
|
|
16
|
+
if (!scroller) return
|
|
17
|
+
|
|
18
|
+
const handleShowBtn = () => {
|
|
19
|
+
const scrollPosition =
|
|
20
|
+
Number(scroller.scrollHeight) - Number(scroller.scrollTop) - Number(scroller.clientHeight)
|
|
21
|
+
|
|
22
|
+
setShowScrollButton(scrollPosition > threshold)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
scroller.addEventListener('scroll', handleShowBtn)
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
scroller.removeEventListener('scroll', handleShowBtn)
|
|
29
|
+
}
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const scrollToBottom = useCallback(() => {
|
|
33
|
+
const { current: scroller } = scrollerRef
|
|
34
|
+
if (!scroller) return
|
|
35
|
+
|
|
36
|
+
scroller.scrollTo({
|
|
37
|
+
top: scroller.scrollHeight,
|
|
38
|
+
behavior: 'smooth'
|
|
39
|
+
})
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
scrollerRef,
|
|
44
|
+
scrollToButtonRef,
|
|
45
|
+
scrollToBottom,
|
|
46
|
+
showScrollButton
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default useScroller
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react'
|
|
2
|
-
import { useMutation } from '@tanstack/react-query'
|
|
2
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { UAParser } from 'ua-parser-js'
|
|
4
4
|
import { v4 } from 'uuid'
|
|
5
5
|
|
|
@@ -7,13 +7,28 @@ import { MessagesService } from '@/src/modules/messages'
|
|
|
7
7
|
import { useGetProfile } from '@/src/modules/profile'
|
|
8
8
|
import { useWidgetLoadingAtom, useWidgetSettingsAtomValue } from '@/src/modules/widget'
|
|
9
9
|
import { MessagesEvents } from '../../events'
|
|
10
|
+
import { useMessagesMaxCount } from '../../store'
|
|
11
|
+
import { setMessagesCache } from '../../utils'
|
|
12
|
+
import { getAllMessagesQuery } from '../use-infinite-get-messages'
|
|
10
13
|
|
|
11
14
|
function useSendTextMessage() {
|
|
12
15
|
const settings = useWidgetSettingsAtomValue()
|
|
13
16
|
const profileQuery = useGetProfile()
|
|
14
17
|
const [, setWidgetLoading] = useWidgetLoadingAtom()
|
|
18
|
+
const queryClient = useQueryClient()
|
|
19
|
+
const limit = useMessagesMaxCount()
|
|
15
20
|
|
|
16
21
|
const userId = useMemo(() => profileQuery.data?.userId?.toString(), [profileQuery.data?.userId])
|
|
22
|
+
const profileId = useMemo(() => profileQuery.data?.id?.toString(), [profileQuery.data?.id])
|
|
23
|
+
const messagesQueryConfig = useMemo(
|
|
24
|
+
() =>
|
|
25
|
+
getAllMessagesQuery({
|
|
26
|
+
conversationId: settings?.conversationId,
|
|
27
|
+
profileId,
|
|
28
|
+
limit
|
|
29
|
+
}),
|
|
30
|
+
[limit, profileId, settings?.conversationId]
|
|
31
|
+
)
|
|
17
32
|
|
|
18
33
|
return useMutation({
|
|
19
34
|
mutationFn(message: string) {
|
|
@@ -64,9 +79,23 @@ function useSendTextMessage() {
|
|
|
64
79
|
}
|
|
65
80
|
})
|
|
66
81
|
},
|
|
67
|
-
onMutate: () => {
|
|
82
|
+
onMutate: (variables) => {
|
|
83
|
+
setMessagesCache({
|
|
84
|
+
queryKey: messagesQueryConfig.queryKey,
|
|
85
|
+
queryClient,
|
|
86
|
+
sending: true,
|
|
87
|
+
data: {
|
|
88
|
+
content: {
|
|
89
|
+
type: 'text/plain',
|
|
90
|
+
text: variables
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})()
|
|
68
94
|
setWidgetLoading(true)
|
|
69
95
|
MessagesEvents.get('c3po-chat:questionSubmitted')?.dispatch()
|
|
96
|
+
},
|
|
97
|
+
onSuccess(data) {
|
|
98
|
+
setMessagesCache({ queryKey: messagesQueryConfig.queryKey, queryClient, data })()
|
|
70
99
|
}
|
|
71
100
|
})
|
|
72
101
|
}
|